@[toc]

继承是面向对象编程中讨论最多的话题。实现继承是ECMAScript唯一支持的继承方式,而这主要是通过原型链实现的。

# 1. 原型链

复习一下 构造函数、原型对象、实例对象之间的关系 在这里插入图片描述

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

SubType.prototype = new SuperType();

// 再将 Sub原型对象的 构造函数 指回来
SubType.prototype.constructor = SubType;

SubType.prototype.getSuperValue = function () {
  return this.subproperty;
};

在这里插入图片描述

let instance = new SubType();

console.log(instance.getSuperValue()); // false
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof Object); // true

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

原型链虽然是实现继承的强大工具,但它也有问题。 主要问题出现在原型中包含引用值的时候。 原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。

原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。

# 2. 盗用构造函数constructor stealing

解决原型链的第一个问题,引用类型值的问题

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

let instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]

解决原型链的第二个问题,传递参数问题

function SuperType(name) {
  this.name = name;
}
function SubType() {
  SuperType.call(this, "yk");
  this.age = 18;
}
let instance1 = new SubType();
console.log(instance1.name); // 'yk'
console.log(instance1.age); // 18

缺点:和构造函数模式一样的问题,所有的方法都在构造函数中定义,因此就无法做到函数的复用。而且超类型的原型中定义的方法,对于子类型而言也是不可见的。

# 3. 组合继承

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。 这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

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;
}
// 原型链 继承方法
SubType.prototype = new SuperType();
// 再将 Sub原型对象的构造函数指回来
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
  console.log(this.age);
};
let i1 = new SubType("yk", 19);
i1.colors.push("black");
console.log(i1.colors); // [ 'red', 'blue', 'green', 'black' ]
i1.sayName() // 'yk'
i1.sayAge() // 19

优点:组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPropertyOf() 也能够用于识别基于组合继承创建的对象。

缺点:调用了两次超类的构造函数,导致基类的原型对象中增添了不必要的超类的实例对象中的所有属性。

# 4. 原型式继承

原型式继承的主要思路是可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型。

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

ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了

let person = {
  name: "yk",
  friends: ["dr", "gjf", "wzc"],
};

let anotherPerson = Object.create(person);
anotherPerson.name = "kk";
anotherPerson.friends.push("yy");

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "gjf";
yetAnotherPerson.friends.push("ycd");

console.log(person.friends); // [ 'dr', 'gjf', 'wzc', 'yy', 'ycd' ]

优点:可以实现基于一个对象的简单继承,不必创建构造函数

缺点:与原型链中提到的缺点相同,一个是传参的问题,一个是属性共享的问题。

# 5. 寄生式继承

寄生式继承的思路是,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回这个对象。

function createAnother(original) {
  let clone = Object.create(original);
  clone.sayHi = function () {
    console.log("hi");
  };
  return clone;
}
let person = {
  name: "yk",
  friends: ["dr", "gjf", "wzc"],
};

let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'

优点: 在主要考虑对象而不是自定义类型和构造函数的情况下,实现简单的继承。

缺点:使用该继承方式,在为对象添加函数的时候,没有办法做到函数的复用。

# 6. 寄生式组合继承

寄生式组合继承与组合继承不同的地方主要是,在继承原型时,我们继承的不是超类的实例对象,而是原型对象是超类原型对象的一个实例对象,这样就解决了基类的原型对象中增添了不必要的超类的实例对象中的所有属性的问题。

function inheritPrototype(subType, superType) {
  // 创建对象  创建原型对象是超类原型对象的一个实例对象
  let prototype = Object.create(superType.prototype);
  // 增强对象 弥补因为重写原型而失去的默认的 constructor 属性
  prototype.constructor = subType;
  // 赋值对象 实现原型继承
  subType.prototype = prototype;
}
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(SubType, SuperType);

SubType.prototype.sayAge = function () {
  console.log(this.age);
};

优点:效率高,避免了在 SubType.prototype 上创建不必要的属性。与此同时还能保持原型链不变,开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

# 参考

《JavaScript高级程序设计(第四版)》

上次更新: 2022/4/21 22:21:34