JavaScript原型

原型是JavaScript实现面向对象编程的核心机制,它允许对象继承其他对象的属性和方法,实现代码复用和共享。理解原型和原型链是掌握JavaScript面向对象编程的关键。

原型的基本概念

在JavaScript中,每个对象都有一个原型(prototype),原型也是一个对象。当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript会自动到其原型对象中查找,这个过程会持续到原型链的末端。

原型链

当访问对象的属性或方法时,JavaScript会按照以下顺序查找:

  1. 首先在对象本身查找
  2. 如果找不到,到对象的原型中查找
  3. 如果还找不到,到原型的原型中查找
  4. 以此类推,直到找到该属性或方法,或者到达原型链的末端(null)

这种链式查找机制称为原型链

构造函数与原型

在JavaScript中,使用构造函数创建对象时,构造函数的prototype属性指向一个对象,该对象是通过该构造函数创建的所有实例的原型。

构造函数的prototype属性

// 定义构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 在构造函数的原型上添加方法
Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

// 使用构造函数创建实例
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

// 实例可以访问原型上的方法
person1.greet(); // Hello, my name is Alice!
person2.greet(); // Hello, my name is Bob!

// 检查实例的原型
console.log(Object.getPrototypeOf(person1)); // Person.prototype
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person2) === Person.prototype); // true

实例的__proto__属性

每个对象实例都有一个__proto__属性(非标准属性,但大多数浏览器支持),指向其原型对象。

// 访问实例的__proto__属性
console.log(person1.__proto__); // Person.prototype
console.log(person1.__proto__ === Person.prototype); // true

// 注意:__proto__是实例的属性,prototype是构造函数的属性
console.log(Person.prototype); // 原型对象
console.log(Person.__proto__); // Function.prototype(构造函数本身的原型)

constructor属性

原型对象上有一个constructor属性,指向创建该原型的构造函数。

// 访问原型的constructor属性
console.log(Person.prototype.constructor); // Person构造函数
console.log(Person.prototype.constructor === Person); // true

// 实例可以通过原型访问constructor属性
console.log(person1.constructor); // Person构造函数
console.log(person1.constructor === Person); // true

原型继承

JavaScript通过原型链实现继承,子类的原型指向父类的实例,从而继承父类的属性和方法。

基于构造函数的继承

// 父类构造函数
function Animal(name) {
  this.name = name;
  this.type = 'Animal';
}

// 父类原型方法
Animal.prototype.makeSound = function() {
  console.log(`${this.name} makes a sound.`);
};

// 子类构造函数
function Dog(name, breed) {
  // 调用父类构造函数,继承实例属性
  Animal.call(this, name);
  this.breed = breed;
  this.type = 'Dog';
}

// 设置子类原型为父类实例,继承原型方法
Dog.prototype = new Animal();

// 修复constructor属性
Dog.prototype.constructor = Dog;

// 子类原型方法
Dog.prototype.bark = function() {
  console.log(`${this.name} barks: Woof!`);
};

// 创建子类实例
const dog = new Dog('Buddy', 'Golden Retriever');

// 访问继承的属性和方法
dog.makeSound(); // Buddy makes a sound.
dog.bark(); // Buddy barks: Woof!
console.log(dog.name); // Buddy
console.log(dog.type); // Dog(子类覆盖了父类的属性)
console.log(dog.breed); // Golden Retriever

// 检查原型链
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

基于Object.create()的继承

ES5引入了Object.create()方法,用于创建一个新对象,指定其原型对象。

// 父类对象
const animal = {
  type: 'Animal',
  makeSound: function() {
    console.log(`${this.name} makes a sound.`);
  }
};

// 使用Object.create()创建子类对象,继承animal
const dog = Object.create(animal);
dog.name = 'Buddy';
dog.breed = 'Golden Retriever';
dog.type = 'Dog';
dog.bark = function() {
  console.log(`${this.name} barks: Woof!`);
};

dog.makeSound(); // Buddy makes a sound.
dog.bark(); // Buddy barks: Woof!

// 检查原型链
console.log(Object.getPrototypeOf(dog) === animal); // true

组合继承

组合继承结合了构造函数继承和原型继承,是JavaScript中最常用的继承方式。

// 父类构造函数
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}

// 父类原型方法
Animal.prototype.makeSound = function() {
  console.log(`${this.name} makes a sound.`);
};

// 子类构造函数
function Dog(name, breed) {
  // 构造函数继承:继承实例属性
  Animal.call(this, name);
  this.breed = breed;
}

// 原型继承:继承原型方法
Dog.prototype = Object.create(Animal.prototype);

// 修复constructor属性
Dog.prototype.constructor = Dog;

// 子类原型方法
Dog.prototype.bark = function() {
  console.log(`${this.name} barks: Woof!`);
};

// 创建实例
const dog1 = new Dog('Buddy', 'Golden Retriever');
const dog2 = new Dog('Max', 'German Shepherd');

// 修改dog1的colors数组,不会影响dog2
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white']

// 访问继承的方法
dog1.makeSound(); // Buddy makes a sound.
dog2.bark(); // Max barks: Woof!

原型对象的方法

JavaScript提供了几个用于操作原型的方法:

1. Object.getPrototypeOf()

返回对象的原型。

const obj = {};
const proto = Object.getPrototypeOf(obj);
console.log(proto === Object.prototype); // true

2. Object.setPrototypeOf()

设置对象的原型。

const obj = {};
const proto = { greet: () => console.log('Hello!') };
Object.setPrototypeOf(obj, proto);
obj.greet(); // Hello!

3. Object.create()

创建一个新对象,指定其原型对象。

const proto = { greet: () => console.log('Hello!') };
const obj = Object.create(proto);
obj.greet(); // Hello!

4. Object.hasOwnProperty()

判断对象是否有指定的自有属性(不包括原型链上的属性)。

const person = {
  name: 'Alice',
  age: 30
};

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('toString')); // false(toString是原型方法)

5. in操作符

判断对象是否有指定的属性(包括原型链上的属性)。

const person = {
  name: 'Alice',
  age: 30
};

console.log('name' in person); // true
console.log('toString' in person); // true(toString是原型方法)
console.log('gender' in person); // false

6. instanceof操作符

判断对象是否是构造函数的实例,基于原型链。

function Person() {}
const person = new Person();

console.log(person instanceof Person); // true
console.log(person instanceof Object); // true
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true

原型的最佳实践

  1. 使用原型共享方法:将公共方法放在原型上,避免每个实例都创建相同的方法,节省内存。
  2. 避免修改内置对象原型:修改Object.prototypeArray.prototype等内置对象的原型可能会导致命名冲突和性能问题。
  3. 使用Object.create()创建对象:创建对象时,优先使用Object.create()方法,明确指定原型。
  4. 修复constructor属性:使用原型继承时,记得修复子类的constructor属性。
  5. 避免深度继承:原型链不宜过长,否则会影响属性查找性能。
  6. 使用组合继承:结合构造函数继承和原型继承,既继承实例属性又继承原型方法。

原型与ES6类的关系

ES6引入了class语法,使JavaScript的面向对象编程更加直观,但底层仍然基于原型机制实现。

ES6类与原型的对应关系

// ES6类
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}!`);
  }
}

// 等价于ES5构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

// ES6类继承
class Student extends Person {
  constructor(name, age, grade) {
    super(name, age);
    this.grade = grade;
  }

  study() {
    console.log(`${this.name} is studying.`);
  }
}

// 等价于ES5组合继承
function Student(name, age, grade) {
  Person.call(this, name, age);
  this.grade = grade;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.study = function() {
  console.log(`${this.name} is studying.`);
};

原型的常见误区

1. 混淆__proto__和prototype

  • __proto__是实例的属性,指向其原型对象
  • prototype是构造函数的属性,指向实例的原型对象
function Person() {}
const person = new Person();

console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

2. 原型上的引用类型属性共享

如果原型上有引用类型属性,所有实例都会共享该属性,修改一个实例的该属性会影响其他实例。

function Person() {
  // 实例属性,每个实例都有独立副本
  this.hobbies = [];
}

// 不推荐:原型上的引用类型属性,所有实例共享
Person.prototype.skills = [];

const person1 = new Person();
const person2 = new Person();

// 修改实例属性,不影响其他实例
person1.hobbies.push('reading');
console.log(person1.hobbies); // ['reading']
console.log(person2.hobbies); // []

// 修改原型上的引用类型属性,影响所有实例
person1.skills.push('coding');
console.log(person1.skills); // ['coding']
console.log(person2.skills); // ['coding'](被影响)

3. 原型链查找性能

原型链查找会影响性能,尤其是原型链较长时。访问对象自身的属性比访问原型链上的属性更快。

const obj = {
  name: 'Alice'
  // 自身属性,访问更快
};

// 原型属性,访问较慢
obj.__proto__.age = 30;

console.log(obj.name); // 自身属性,更快
console.log(obj.age); // 原型属性,较慢

原型的应用场景

1. 实现继承

原型链是JavaScript实现继承的主要方式,允许子类继承父类的属性和方法。

2. 共享方法

将公共方法放在原型上,所有实例共享,节省内存。

function Circle(radius) {
  this.radius = radius;
}

// 公共方法放在原型上,所有实例共享
Circle.prototype.getArea = function() {
  return Math.PI * this.radius * this.radius;
};

const circle1 = new Circle(5);
const circle2 = new Circle(10);

console.log(circle1.getArea()); // 78.53981633974483
console.log(circle2.getArea()); // 314.1592653589793
console.log(circle1.getArea === circle2.getArea); // true(共享同一个方法)

3. 扩展内置对象

虽然不推荐,但可以通过修改原型扩展内置对象的功能。

// 扩展Array原型,添加求和方法
Array.prototype.sum = function() {
  return this.reduce((acc, current) => acc + current, 0);
};

const numbers = [1, 2, 3, 4, 5];
console.log(numbers.sum()); // 15

4. 创建对象池

使用原型创建对象池,复用对象,提高性能。

// 对象池
function ObjectPool() {
  this.pool = [];
}

ObjectPool.prototype.get = function() {
  return this.pool.length > 0 ? this.pool.pop() : {};
};

ObjectPool.prototype.release = function(obj) {
  // 清空对象属性
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      delete obj[key];
    }
  }
  this.pool.push(obj);
};

// 使用对象池
const pool = new ObjectPool();
const obj1 = pool.get();
obj1.name = 'Alice';
console.log(obj1.name); // Alice

pool.release(obj1);
const obj2 = pool.get();
console.log(obj2.name); // undefined(对象已被清空)
console.log(obj1 === obj2); // true(复用了同一个对象)

总结

原型是JavaScript实现面向对象编程的核心机制,它通过原型链实现对象继承和属性方法共享。理解原型和原型链是掌握JavaScript面向对象编程的关键。

  • 每个对象都有一个原型,原型也是一个对象
  • 构造函数的prototype属性指向实例的原型
  • 实例的__proto__属性指向其原型
  • 原型链是属性查找的链式机制
  • 可以通过原型实现继承和共享方法
  • ES6的class语法底层基于原型机制

掌握原型和原型链的概念,有助于编写更高效、更可维护的JavaScript代码,特别是在面向对象编程和组件开发中。

« 上一篇 JavaScript解构赋值 下一篇 » JavaScript类