JS 各种继承方法及其优缺点

220 阅读7分钟
写在前面

本文是近期学习总结的一套关于 JavaScript 继承方式的几种方法以及优缺点。
你即将收获
- JavaScript 中几种继承方式的实现原理, 以及各种继承方式的优缺点。
- 理解 JavaScript 中几种继承方式之间的联系。
在此, 极力推荐 《JavaScript高级程序设计》、《# ECMAScript 6 入门

话不多说, 开始码!

1. 原型链继承
  /*
    构造函数、原型和实例之间的关系: 每个构造函数都有一个原型对象, 原型对象都包含一个指向构造函数的
    指针, 而实例都包含一个原型对象的指针。
    继承的本质就是: 复制(即重写原型对象), 代之以一个新类型的实例。
  */
  function Child() {
    this.name = "Arvin";
  }
  
  Child.prototype.getName = function () {
    return this.name;
  }
  
  function newChild() {
    this.newName = "Arvin1";
  }
  // 关键之处 --> 创建Child实例(构造函数), 并将该实例赋值给到 newChild的prototype(原型)
  newChild.prototype = new Child();
  newChild.prototype.getNewName = function () {
    return this.newName;
  }
  
  var params = new newChild();
  console.log("name值====", params.getName()); // Arvin

原型链方式书写相对简单, 但是存在缺点:

  • 原型链继承最大的问题就是对于引用类型的值的原型。引用类型值的原型属性会被所有的实例共享。一句话概括就是: 多个实例对引用类型的操作会被篡改。
  • 构造函数实例化对象无法进行参数传递
    下面我们来整个例子
  function Child() {
    this.proData = ["25", "Arvin", "男"]; // 引用类型数据
  }
  
  function newChild() {}
  // 关键之处 --> 创建name实例(构造函数), 并将该实例赋值给到 newName的prototype(原型)
  newChild.prototype = new Child();
  var params = new newChild();
  // 往 name 中的 proData 属性添加一个新属性
  params.proData.push("max"); // 实际上是操作 new newChild() 的原型上的属性
  console.log("params--name值====", params.proData); // ["25", "Arvin", "男", "max"]
  var params1 = new newChild();
  console.log("params1--name值====", params1.proData); // ["25", "Arvin", "男", "max"]
  // 由于 params, params1都是通过 new newChild()的方式取值(都是同一个实例), 所以结果一致 
2. 构造函数继承
  function Child() {
    this.color = ["red", "blue", "green"];
  }
  function ChildCall() {
    Child.call(this); // 继承 Arvin, 核心的一步
  }
  var params1 = new ChildCall();
  params1.color.push("yellow");
  console.log("params1更新后的值=====", params1.color); // ["red", "blue", "green", "yellow"]

  var params2 = new ChildCall();
  console.log("params2当前值=====", params2.color); // ["red", "blue", "green"]

构造函数继承方式核心代码就是 Child.call(this);, 创建子类实例时创建 Child 构造函数, 在ChildCall 的每个实例中都会将 Arvin 中的属性复制一份。
缺点:

  • 构造函数继承方式, 只能继承父类的实例, 不能够继承原型上的属性和方法。
  • 无法复用, 每个子类都有父类实例函数的副本, 这对于性能方面就比较差。

优点:

  • 避免了引用类型的属性被所有实例共享的问题。
  • 可以在构造函数实例化对象传参。
3. 组合继承

所谓的组合继承, 其实就是将 原型链继承构造函数继承 两种继承方法合并, 使用构造函数来实现实例属性的继承。

  function Child(name) {
    this.name = "传入值--" + name;
    this.color = ["red", "blue", "green"];
  }

  Child.prototype.getName = function () {
    console.log(this.name);
  }
  
  function ChildCall(name, theme) {
    // 在这里通过改变this指向实现继承 Child 的属性
    Child.call(this, name); // 赋值 第二次调用
    this.theme = theme; // 添加新属性
  }

  // 继承 Child 中的方法--> 设置子类型实例原型
  ChildCall.prototype = new Child(); // 第一次调用
  // 重写 ChildCall.prototypr的constructor属性, 指向自己的构造函数 ChildCall
  // ChildCall.prototype.constructor = ChildCall;
  ChildCall.prototype.getTheme = function () {
    console.log("theme===", this.theme);
  }

  let params = new ChildCall("Arvin", "dark"); // 创建子类型实例
  params.color.push("yellow");
  console.log("params===", params, params.color);

  let params1 = new ChildCall("艾文", "light"); // 创建子类型实例
  console.log("params1===", params1, params1.color);

微信图片编辑_20220630105521.jpg 缺点:

  • 组合模式的缺点就是在使用子类创建实例对象时, 其原型中会存在两份相同的属性和方法。
  • 组合继承最大的缺点就是会调用两次父构造函数

优点:

  • 融合原型链继承和构造函数的优点, 是 Javascript 中最常用的继承模式。
4. 原型式继承

原型式继承实现很简单, 就是利用一个空对象作为中介, 将某个对象直接赋值给空对象构造函数的原型。

   function createObj(obj) {
     // createObj() 对传入的参数(obj)执行一次潜复制, 将构造函数的 F 的原型直接指向传入的对象。
     function F() {} // 创建一个空函数
     F.prototype = obj; // 将该函数的原型指向传入的参数 obj
     return new F(); // 返回构造函数
   }
   
   var person = {
     name: "Arvin",
     motto: "减低期待, 减少依赖 !",
     other: [20, 25, 60, 55]
   }
 
   var personOne = createObj(person);
   personOne.name = "personOne--Arvin";
   personOne.other.push("Arvin--one");
   console.log(personOne, personOne.name, personOne.other); // F {name: "personOne--Arvin"}, "personOne--Arvin", [20, 25, 60, 55, "Arvin--one"]
 
   var personTwo = createObj(person);
   personTwo.name = "personTwo--Arvin";
   personTwo.other.push("Arvin--two");
   console.log(personTwo, personTwo.name, personTwo.other); // F {name: "personTwo--Arvin"}, "personTwo--Arvin", [20, 25, 60, 55, "Arvin--one", "person-two"]

微信图片_20220630160849.png 看上图输出结果我们可以看到, 修改 personOne.name 的值, personTwo.name 的值并没有发生改变, 欸很奇怪, 没达到预期, 就很棒! 其实这里原因就在 createObj 这个方法, 因为每次通过new操作符都会新建一个实例, 所以 personOne.name 这个操作其实是给 personOne 新增了一个 name 属性而已, 并非修改器原型上的 name 的值。
欸! other 这个属性的值居然发生了改变(因为原型链继承多个实例的引用类型属性指向相同, 存在篡改的可能), 这也是 原型式继承 的弊端之一。
缺点:

  • 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
  • 无法传递参数。
5. 寄生式组合

创建一个仅用于封装继承过程的函数, 该函数在内部一某种形式来做增强对象, 最后将对象返回。

  function createObj(obj) {
    var newObj = Object.create(obj); // 使用 ES5 的 Object.create()方法创建新对象
    newObj.sayName = function (data) { // 增强对象
      console.log("我是 Arvin", data);
    }
    return newObj; // 返回对象
  }

  var person = {
    name: "Arvin",
    colors: ["green", "red", "blue"]
  }
  var personOne = createObj(person);
  console.log("personOne===", personOne); // {syaName: f}
  personOne.sayName("哈哈"); // 我是 Arvin 哈哈

  var personTwo = createObj(person);
  personTwo.colors.push("yellow");
  console.log("personTwo===", personTwo, personTwo.colors);
  personTwo.sayName();

输出结果如下图:

微信图片_20220630170720.png 如上图数据结果可以看出 寄生式继承和原型式继承缺点是一样的:

  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
  • 无法传递参数
6. 寄生组合式继承---结合构造函数传递参数和寄生模式实现继承
  function initPrototype(child, parent) {
    var prototype = Object.create(parent.prototype); // 创建对象, 拿到父类实例的原型对象
    prototype.constructor = child; // 增强对象, 避免重写的时候 constructor 属性存在空值情况, 直接将子类赋值过去
    child.prototype = prototype; // 指定对象, 子类的原型对象指向父类的原型对象也就是新创建的对象 prototype
  }

  // 创建父类实例
  function parent(name) {
    this.name = name;
    this.colors = ["blue", "red", "grey"]
  }
  // 在父类实例原型对象上添加方法
  parent.prototype.getName = function () {
    console.log('父类实例方法=====', this.name);
  }

  // 创建子类实例--支持传参
  function child(name, theme) {
    parent.call(this, name); // 构造函数, 增强子类实例, 支持传递参数避免篡改现象
    this.theme = theme;
  }

  /*
    在这里需要注意的是, 想要在子类原型上新增属性, 需要在父类原型指向子类这个方法(initPrototype)之后才行, 否则会报错
    因为上面我们已经讲过, 子类原型是指向父类的原型上的, 所以你在 initPrototype 方法执行之前必然会报错。
  */
  initPrototype(child, parent); // 调用方法, 将父类的原型指向子类
  child.prototype.getChildName = function () {
    console.log(this.theme);
  }

  var params1 = new child("Arvin", "dark");
  console.log("params1======", params1); // params1====== child {name: "Arvin", colors: Array(3), theme: "dark"}
  params1.getChildName(); // dark

微信图片_20220701100345.png 优点:    来自于《JavaScript高级程序设计》

  • 目前就所有的继承方式而言, 寄生组合式继承式最成熟的, 而且很多库的实现也是用该方法。
  • 寄生组合式继承, 效率高,在实例代码中只调用了一次 Parent 构造函数, 这也避免了在 child 的原型上 prototype 创建不必要的属性。
  • 同时保持原型链不变, 并且能够正常使用 instanceof 和isPrototypeOf()

同时还有两种继承的方法:

  • ES6 Object.assign(), 混入方式继承多个对象
  • ES6 extends, ES6 类继承 这两个方法对于有看过 ES6文章的同学, 肯定很熟悉,我就不再做出介绍。如果没有看过的同学, 我在下面贴出了ES6官方文档地址, ES6入门教程