Skip to content

继承(Inheritance)

认识构造函数、原型对象和实例

每个构造函数都有一个原型对象,这个原型对象中又存在一个 constructor 属性,值为该构造函数。

每个实例对象都有一个指向该原型对象的指针 [[protoType]] (在许多浏览器实现中, 可以使用 __proto__ 非标准属性来访问)。

TIP

通过 Object.getPrototypeOf(instance) 可以获取到原型对象 [[prototype]]

以原型链的方式实现继承

javascript
function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function () {
  return this.property;
};
function SubType() {
  this.subproperty = false;
}
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

SubType.prototype = new SuperType();
var instance = new SubType();
console.log(instance.getSuperValue()); // true

instance 实例由 SubType 构造函数创建,它的原型对象指向了 SuperType 原型对象, instance 实例通过原型链可以访问父类的方法和属性。

值得注意的是,SubType 构造函数中的 this.property = true; 语句执行后, property 属性被添加到了 SubType 原型对象中,而不是 instance 对象本身中。

由于 constructor 属性存在于原型对象上, 而 SubType 的原型对象被重写为 SuperType 的实例, 且 instance 又是 SubType 的实例, 因此 instance 对象的 [[prototype]] 中的 constructor 指向了 SuperType

小结

  • 子类的原型对象 prototype 被重写为父类构造函数的实例
  • 子类实例 [[prototype]] 中的 constructor 指向父类的 constructor
  • 父类原型方法仍然存在父类的原型对象中,但是其构造函数中为 this 赋值的属性却存在于子类的原型对象中。

练习

javascript
Object.getPrototypeOf(Object.prototype) === null; // true
// Object 也是一个函数
// Object 函数签名: Object([value])
// Object.length === 1, 函数的 length 属性始终为函数签名中的参数个数
Object.getPrototypeOf(Object) === Function.prototype; // true

默认原型

在默认情况下,任何函数的原型对象都是一个 Object 的实例。

确定原型和实例的关系

通过 instanceof 关键字

如果 instance 的原型链中包含 SomeFn 构造函数的原型,则返回 true。

语法

javascript
instance instanceof Somefn;

代码示例

javascript
instance instanceof Object; // true
instance instanceof SuperType; // true
instance instanceof SubType; // true

代码实现

javascript
/**
 * 将实例的 [[prototype]] 和构造函数的 prototype 进行比较,直至原型链的末尾。
 * 可以看到不需要访问原型对象中的 constructor 属性。
 * @param left 实例对象
 * @param right 构造函数
 */
function MyInstanceOf(left, right) {
  let prototype = right.prototype;
  while (left !== null) {
    if (left === prototype) return true;
    left = Object.getPrototypeOf(left);
  }
  return false;
}

通过 isPrototypeOf 方法

测试一个对象是否存在于某个构造函数的原型链上。

语法

javascript
SomeFn.prototype.isPrototypeOf(instance);

代码示例

javascript
Object.prototype.isPrototypeOf(instance); // true
SuperType.prototype.isPrototypeOf(instance); // true
SubType.prototype.isPrototypeOf(instance); // true

代码实现

javascript
function isPrototypeOf(proto, object) {
  while (object !== null) {
    if (object === proto) return true;
    object = Object.getPrototypeOf(object);
  }
  return false;
}
// 测试一下
function Person() {}
const person = new Person();
isPrototypeOf(Object.prototype, person); // true
isPrototypeOf(Person.prototype, person); // true
isPrototypeOf(Array.prototype, person); // false

重写父类方法

SubType.prototype = new SuperType() 之后,添加对应的同名方法以覆盖方法, 原理是基于 JavaScript 的委托机制。

使用字面量赋值给原型对象

如果使用一个字面量对象赋值给原型对象,那么原有的原型赋值会无效。

javascript
SubType.prototype = new SuperType();
// 使用字面量会导致上面的原型赋值无效,可以理解为[[prototype]]指针指向了另一个对象。
SubType.prototype = {
  // ...
};

原型链继承附带字面量对象

javascript
/**
 * 一个简易的实现,保持继承父类,并且添加对象字面量中的属性。
 * 对象字面量中的属性会覆盖父类原型对象中的同名属性
 * @param subtype 子类构造函数
 * @param superConstrucotr 父类构造函数
 * @param object 对象字面量
 */
function MyExtends(subtype, superConstrucotr, object) {
  subtype.prototype = new superConstrucotr();
  for (key in object) {
    subtype.prototype[key] = object[key];
  }
}

原型链继承的缺点

  1. 原型链继承会导致原型链中的引用类型的值会被所有实例共享
  2. 子类在实例化时无法为父类构造函数传入参数,相当于只能使用无参构造

共享引用类型

在原型链中存在的引用类型的值会被所有实例共享,使用上面的原型链继承时,子类的原型实际上变成了父类构造函数的一个实例。

javascript
function SuperType() {
  this.colors = ['red', 'green', 'pink', 'black'];
}

function SubType() {}

SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.colors.push('blue');
console.log(instance1.colors); // "red,green,pink,black,blue"

const instance2 = new SubType();
console.log(instance2.colors); // "red,green,pink,black,blue"

构造函数传参

我们可以发现,在基于原型链继承时,子类在实例化时无法为父类的构造函数传入参数。

经典继承

又叫盗用构造函数、对象伪装,在子类构造函数中调用父类的构造函数,通过 call 或者 apply

javascript
function SuperType() {
  this.colors = ['red', 'green', 'pink', 'black'];
}

function SubType() {
  SuperType.call(this);
}
const instance1 = new SubType();
instance1.colors.push('blue');
console.log(instance1.colors.toString()); // "red,green,pink,black,blue"

const instance2 = new SubType();
console.log(instance2.colors.toString()); // "red,green,pink,black"

优缺点

优点:

  1. 子类实例化时可以为父类构造函数传递参数
  2. 父类引用类型的属性不再被所有实例共享
  3. 可以实现多继承,使用 call 去调用多个父类构造函数

缺点:

  1. 子类实例无法访问父类原型中定义的方法
  2. 生成的实例仅仅是子类的实例,而不是父类的实例,instance1 instanceof SuperType 结果为 false
  3. 父类如果想要定义子类能够访问的方法,只能在父类构造函数的内部去定义方法,因此每个实例都有一个方法的副本,导致函数不能被复用,造成了额外的开销

组合继承

又叫伪经典继承。使用原型链继承原型上的属性和方法,使用盗用构造函数继承实例属性。

javascript
function SuperType(name) {
  // 值类型的属性
  this.name = name;
  // 引用类型的属性
  this.colors = ['red', 'blue'];
}
// 在父类原型对象上定义方法
SuperType.prototype.getName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  // 调用一次父类构造函数,并传递了参数
  SuperType.call(this, name);
  this.age = age;
}
// 再次调用了父类构造函数
SubType.prototype = new SuperType();

优缺点

优点:

  1. 子类实例的属性是独立的,引用类型的属性不在所有实例间共享
  2. 子类实例可以访问父类原型上定义的方法
  3. 子类实例可以正确的被 instanceofisPrototypeOf 识别
  4. 可以为父类构造函数传递参数

缺点:

  1. 父类构造函数被调用了两次
  2. 父类构造函数中为 this 添加的属性不仅存在于新实例中,还存在于子类原型对象上

原型式继承

来源于 Douglas Crockford 于 2006 年写的文章《Prototypal Inheritance in JavaScript》,文中最终给出了一个函数:

javascript
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

示例

javascript
// 没有创建和使用构造函数,但是在多个实例之间共享了信息
const person = {
  name: 'Joh',
  friends: ['Bob', 'Alice'],
};
const anotherPerson = Object.create(person);
anotherPerson.name = 'Grace';
anotherPerson.friends.push('Alex');

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Van');

// 引用类型会被所有实例所共享
console.log(person.friends.toString()); // "Bob,Alice,Alex,Van"

使用场景

基于一个已有对象创建一个新对象。将已有对象传递给 object 函数,然后基于返回的对象进行修改。

原型式继承适合不需要单独创建构造函数,但是需要在对象间共享信息的场合使用,引用类型会被所有实例共享。

TIP

在 ES6 中,使用 Object.create 传递单个参数和这里的 object 函数效果是一样的。

寄生式继承

创建一个实现继承的函数,在函数内部增强对象,并且返回这个对象

javascript
// 一个可以生成对象的函数
function createAnother(original) {
  // 基于 original 创建新对象
  const clone = Object.create(original);
  // 增强新对象
  clone.sayHello = function () {
    console.log('hello');
  };
  // 返回新对象
  return clone;
}

使用场景

寄生式继承适合只关注对象,而不在乎类型和构造函数的场景。

寄生组合式继承

上文中的组合继承算是比较好的继承方式,但有着父类构造函数被调用两次的缺点,寄生组合式继承算是对其的完善。

基本思路是,不通过父类构造函数给子类原型赋值,而是取得父类原型的一个副本。即使用寄生式继承来继承父类原型,然后将其返回的新对象赋值给子类原型。

javascript
// 寄生式继承来继承父类原型
function inheritPrototype(subType, superType) {
  const prototype = Object.create(superType.prototype); // 创建对象
  // 重写 constructor 来进行修正,一个对象的原型对象上的 constructor 应该指向其构造函数
  prototype.constructor = subType; // 增强对象
  subType.prototype = prototype; // 赋值对象
}
javascript
// 完整的实例代码
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
// 在父类原型对象上添加方法,以便所有的实例对象访问这个函数
SuperType.prototype.sayName = function () {
  console.log(this.name);
};

function SubType(name, age) {
  // 向父类构造函数传递参数
  SuperType.call(this, name);
  // 子类有着自己新增的属性
  this.age = age;
}
// 采用寄生式继承去继承父类原型对象,这种方式不会再次调用父类构造函数
inheritPrototype(SuperType, SuperType);
SubType.prototype.sayAge = function () {
  console.log(this.age);
};

解决的问题

  1. 只调用一次父类构造函数
  2. 子类原型对象不会添加额外用不上的属性
  3. 原型链仍然可以使用,即定义在原型链上的方法可以被实例对象所共享
  4. instanceofisPrototypeOf 可以获得正确的结果
  5. 解决了原型链继承时,[[prototype]] 中的 constrctor 指向父类构造函数的问题

参考资料

  1. JavaScript 高级程序设计
  2. MDN