一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合式/ES6类继承

213 阅读5分钟

继承

JavaScripy常见的继承方式

  • 原型链继承
  • 构造函数继承(借助 call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • ES6类继承

关于构造函数/原型对象/对象原型/原型链基础知识还不是特别明晰的小伙伴推荐阅读这篇文章 一篇文章理解——构造函数/原型对象/对象原型/原型链 - 掘金 (juejin.cn)

原型链继承

核心:将父类的实例作为子类的原型

所有涉及到原型链继承的继承方式都要修改子类构造函数的指向, 否则子类实例的构造函数会指向Animal

function Animal() {
  this.colors = ["blue", "pink"];
}

Animal.prototype.getColor = function () {
  return this.colors;
};

function Dog() {}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

let dog1 = new Dog();
dog1.colors.push("white");
console.log(dog1.colors); //'blue', 'pink', 'white']
let dog2 = new Dog();
console.log(dog2.colors); //'blue', 'pink', 'white']

优点:父类方法可以复用 缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

构造函数继承

核心:将父类构造函数的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到prototype的继承

借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中(构造函数继承无法继承父类原型上的方法),所以会导致每次创建子类实例都会创建一遍方法。

function Animal(name) {
  this.name = name;
  this.getName = function () {
    return this.name;
  };
}

function Dog(name) {
  Animal.call(this, name);
}

优点:和原型链继承完全反过来。

  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。

组合继承

核心:原型链继承和构造函数继承的组合,兼具了二者的优点。

实现方法共享和属性的独立

function Animal(name) {
  this.name = name;
  this.colors = ["blue", "pink"];
}

Animal.prototype.getColor = function () {
  return this.colors;   
};

function Dog(name, age) {
  Animal.call(this, name);
  this.age = age;
}   //第二次调用Animal

Dog.prototype = new Animal();    //第一次调用Animal
Dog.prototype.constructor = Dog;

let dog1 = new Dog("Lucky", 2);
dog1.colors.push("white");
console.log(dog1);
console.log(dog1.getColor());
let dog2 = new Dog("Coco", 1);
console.log(dog2);
console.log(dog2.getColor());

控制台打印输出:

image.png

优点:

  • 父类的方法可以被复用
  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点: 调用了两次父类的构造函数,第一次给子类的原型添加了父类的name, colors属性,第二次又给子类的构造函数添加了父类的name, colors属性,从而覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。

原型式继承

核心:原型式继承的object方法本质上是对参数对象的一个浅复制

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

const animal = {
  name: "unkown",
  colors: ["blue", "pink"],
};

const dog1 = object(animal);
dog1.name = "Tom";
dog1.colors.push("white");
const dog2 = object(animal);
dog2.name = "Jerry";
console.log(dog1.name); //Tom
console.log(dog1.colors); //['blue', 'pink', 'white']
console.log(dog2.name);   //Jerry
console.log(dog2.colors);  //['blue', 'pink', 'white']

object内部首先是创建了一个空的构造函数F,然后把F的prototype指向参数proto,最后返回一个F的实例对象,完成继承. 原型式继承看起来跟原型继承很像,事实上,两者因为都是基于prototype继承的,所以也有一些相同的特性,比如引用属性共享问题, 那原型式继承原型链继承有什么区别呢? 一个比较明显的区别就是object函数接收的参数不一定要是构造函数,也可以是其他任何对象, 这样我们就相当于是浅复制了一个对象.

es5的Object.create()函数,就是基于原型式继承的

原型式继承是道格拉斯-克罗克福德 2006 年在 Prototypal Inheritance in JavaScript一文中提出的

寄生式继承

核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

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

function createAnother(animal) {
  const clone = object(animal);
  clone.sayHi = function () {
    console.log("Hi");
  };
  return clone;
}

const animal = {
  name: "unkown",
  colors: ["blue", "pink"],
};

const dog1 = createAnother(animal);
dog1.name = "Tom";
dog1.colors.push("white");

const dog2 = createAnother(animal);
dog2.name = "Jerry";

dog1.sayHi(); //"Hi"
dog2.sayHi(); //"Hi"
console.log(dog1.name); //Tom
console.log(dog1.colors); //['blue', 'pink', 'white']
console.log(dog2.name); //Jerry
console.log(dog2.colors); //['blue', 'pink', 'white']

优缺点:仅提供一种思路,没什么优点

寄生组合式继承

上一篇我们提到的组合继承其实也有个缺点,就是父类构造函数里面的代码会执行2遍,第一遍是在原型继承的时候实例化父类, 第二遍是在子类的构造函数里面借用父类的构造函数,我们可以用寄生组合式继承来解决这个问题

function Animal(name) {
  this.name = name;
  this.colors = ["blue", "pink"];
}

Animal.prototype.getColor = function () {
  return this.colors;
};

function Dog(name, age) {
  Animal.call(this, name);
  this.age = age;
} //第二次调用Animal

// Dog.prototype = new Animal(); //第一次调用Animal
// Dog.prototype.constructor = Dog;

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

function inheritPrototype(child, parent) {
  let prototype = object(parent.prototype); // 创建了父类原型的浅复制
  prototype.constructor = child; // 修正原型的构造函数
  child.prototype = prototype; // 将子类的原型替换为这个原型
}

inheritPrototype(Dog, Animal);

let dog1 = new Dog("Lucky", 2);
dog1.colors.push("white");
console.log(dog1);
console.log(dog1.getColor());
let dog2 = new Dog("Coco", 1);
console.log(dog2);
console.log(dog2.getColor());

image.png

我们用 inheritPrototype 函数替换了 Dog.prototype = new Animal(); 从而避免了执行 new Animal().

书上说: 寄生组合式继承是引用类型最理想的继承范式.

ES6 Class extends继承

核心: ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ["blue", "pink"];
  }
  getName() {
    return this.name;
  }
}

class Dog extends Animal {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}

let dog1 = new Dog("Lucky", 2);
dog1.colors.push("white");
console.log(dog1.colors); // ['blue', 'pink', 'white']
console.log(dog1.age); // 2
console.log(dog1.getName()); //"Lucky"
let dog2 = new Dog("Coco", 1);
console.log(dog2);
console.log(dog2.getName()); // "Coco"
console.log(dog2.colors); // ['blue', 'pink']
console.log(dog2.age); // 1
console.log(dog1.getName === dog2.getName); //true

总结

原型链继承和原型式继承有一样的优缺点,构造函数继承与寄生式继承也相互对应。寄生组合继承基于Object.create, 同时优化了组合继承,成为了完美的继承方式。ES6 Class Extends的结果与寄生组合继承基本一致,但是实现方案又略有不同

image.png