继承的艺术: 探索JS中实现继承的多种方式

在JavaScript的世界中,继承不仅是面向对象编程中的基石,更是一门优雅的艺术。继承可以让我们构建出灵活、可扩展的代码结构,以及更好地复用以前的开发代码缩短开发的周期提升开发效率

b51b4265eb333689c2e05d2311a1a186.jpg

可以看到,小狗、小猫、小鸡都继承了动物这个类,也就是都获得了动物类的特征,此外它们还具有各自特有的一些特征和行为。同样地,在JavaScript中对象也可以通过继承其他对象的属性和方法来扩展其功能

一、原型链继承

原型链继承是JavaScript中最基本都继承方式,其主要是将一个类型的实例对象赋值给另一个类型的原型。然而每个类型的实例又都有一个指向其原型对象的内部指针(通常通过__proto__属性来访问),进而会形成一个原型链。子类对象就可以通过这个原型链进行属性和方法的查找,从而实现继承


  function Parent() {

    this.name = 'parent';

    this.ref = [1, 2, 3]; // 引用类型值会被所有子类实例共享

  }

  Parent.prototype.printName = function() {console.log(this.name)} // 父类原型中的方法



  function Child() {

    this.name = 'child'; // 继承自Parent类的属性

    this.age = 18; // 子类扩展属性

  }

  Child.prototype = new Parent(); // 通过将Parent类的实例对象作为Child类的原型来实现继承



  var child1 = new Child();

  var child2 = new Child();

  child1.printName(); // child,说明继承了父类原型中的方法

  child1.ref.push(4); // child1实例对象往ref属性中添加一个4

  console.log(child1.ref, child2.ref); // [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ] 会互相影响

可以看到,原型链继承的缺点就是父类中的引用属性值会被所有子类实例共享,也就是说不同的子类实例对父类中的引用属性值修改时会互相影响。因为它们修改的是同一个父类实例

二、借用构造函数继承

借用构造函数继承主要是通过在子类构造函数中调用父类构造函数,也就是借用父类的构造函数


  function Parent() {

    this.name = 'parent';

    this.ref = [1, 2, 3]; // 引用类型值不会被所有子类实例共享

  }

  Parent.prototype.printName = function() {console.log(this.name)} // 父类原型中的方法

  function Child() {

    Parent.call(this); // 借用父类构造函数继承父类构造函数中定义的属性

    this.name = 'child';

    this.age = 18; // 子类扩展属性

  }

  var child1 = new Child();

  var child2 = new Child();

  child1.ref.push(4); // child1实例对象往ref属性中添加一个4

  console.log(child1.ref, child2.ref); // [ 1, 2, 3, 4 ] [ 1, 2, 3 ] 不会互相影响

  console.log(child1.printName()); // 会报错,Uncaught TypeError: child.printName is not a function

可以看到,借用构造函数继承解决了原型链继承会共享父类引用属性的问题。也就是每个子类实例中都有一份属于自己的引用属性互相之间不会影响。但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法不能继承父类原型中的属性或者方法

三、组合继承

组合继承主要是将原型链继承和构造函数继承的优点结合在一起来实现继承。也就是说,通过借用构造函数来实现对实例属性的继承通过使用原型链实现对原型属性和方法的继承


  function Parent() {

    this.name = 'parent';

    this.ref = [1, 2, 3];

  }

  Parent.prototype.printName = function() {console.log(this.name)}



  function Child(){

    Parent.call(this); // 借用父类构造函数的时候又会调用一次父类构造函数

    this.name = 'child';

    this.age = 18;

  }

  Child.prototype = new Parent(); // 创建父类实例的时候会调用一次父类构造函数



  var child1 = new Child();

  var child2 = new Child();

  child1.ref.push(4);

  console.log(child1.ref, child2.ref); // [ 1, 2, 3, 4 ] [ 1, 2, 3 ],不会互相影响

  console.log(child1.printName()); // child,可以继承了父类原型中的方法

可以看到,组合继承同时解决了原型链继承会共享父类引用属性以及借用构造函数无法继承父类原型属性的问题。但是随之而来的缺点也比较明显——会执行两次父类的构造函数,也就会产生额外的性能开销

四、原型式继承

原型式继承是基于现有的对象来创建一个新的对象,并且将新对象的原型设置为这个现有的对象。这样新创建的对象就可以访问现有对象的属性和方法了。也就是用现有对象作为模版复制出一个新的对象。而JavaScript中有现成的Object.create可以实现原型式继承


  const parent = { // 现有对象

    name: "parent",

    ref: [1, 2, 3],

    printName: function() {

        console.log(this.name);

    }

  }



  const child1 = Object.create(parent); // 原型式继承基于现有的parent对象复制一个新的对象

  child1.name = "child1";

  child1.ref.push(4);

  child1.printName(); // child1



  const child2 = Object.create(parent); // 原型式继承基于现有的parent对象复制一个新的对象

  child2.name = "child2";

  child2.ref.push(5);

  child2.printName(); // child2



  console.log(child1.ref, child2.ref); // [1, 2, 3, 4, 5] [1, 2, 3, 4, 5],互相影响

可以看到,原型式继承和原型链继承非常类似,所以它们都有一个共同的缺点所有子类实例都会共享父类引用属性。原型链继承是基于构造函数来实现,而原型式继承是基于现有对象来实现。

五、寄生式继承

寄生式继承主要是在原型式继承的基础上进行增强,比如添加一些新的属性和方法


  const parent = {

    name: "parent",

    ref: [1, 2, 3],

    printName: function() {

        console.log(this.name);

    }

  }

  function clone(proto) {

    const clone = Object.create(proto);

    // 在原型式继承的基础上添加一些自定义的属性和方法

    clone.customFn = function() {

        return this.ref;

    };

    return clone;

  }

  const child = clone(parent);

  child.printName(); // parent

  child.customFn(); // [1, 2, 3]

可以看到,寄生式继承并没有创建新的类型,而是对原有对象进行了包装或增强,返回的是一个增强了功能的对象实例

六、寄生组合式继承

寄生组合式继承主要是将寄生式继承和组合继承的优点结合在一起来实现继承


  function Parent() {

    this.name = 'parent';

    this.ref = [1, 2, 3];

  }

  Parent.prototype.printName = function() {console.log(this.name)}

  function Child() {

    Parent.call(this); // 借用构造函数继承

    this.name = 'child';

  }

  function clone (parent, child) {

    // 通过 Object.create 原型式继承来解决组合继承中两次调用父类构造函数的问题

    child.prototype = Object.create(parent.prototype); // 原型式继承,不会调用父类构造函数

    child.prototype.constructor = child;

  }



  clone(Parent, Child);

  Child.prototype.customFn = function () { // 寄生式继承

    console.log(this.ref)

  }

  const child1 = new Child();

  const child2 = new Child();

  child1.ref.push(4);

  child1.printName(); // child

  child2.customFn(); // [1, 2, 3]

  console.log(child1.ref, child2.ref); // [ 1, 2, 3, 4 ] [ 1, 2, 3 ],不会互相影响

可以看到,寄生组合式继承通过原型式继承来解决组合继承中两次调用父类构造函数的问题减少了性能的开销。可以说寄生组合式继承是这六种里面最优的继承方式

七、ES6中的类继承

ES6中新增了extends关键字来实现类继承


  class Parent { // 定义一个父类



  }



  class Child extends Parent { // 类继承

      constructor() {

          super();

      }

  }

需要注意的是,extends本质是一个语法糖,并且存在兼容性问题在不支持ES6语法的浏览器中需要通过babel转换为ES5才能执行


  function _inherits(subClass, superClass) {

      // 使用原型式继承

      subClass.prototype = Object.create(superClass && superClass.prototype, {

        constructor: { value: subClass, writable: true, configurable: true },

      });

      if (superClass) _setPrototypeOf(subClass, superClass);

  }

  function _createClass(Constructor, protoProps, staticProps) {

      if (protoProps) _defineProperties(Constructor.prototype, protoProps);

      return Constructor;

  }

  let Parent = _createClass(function Parent() {

      _classCallCheck(this, Parent);

  });

  let Child = (function (_Parent) {

      function Child() {

        _classCallCheck(this, Child);

        return _callSuper(this, Child, arguments); // 借用父类构造函数

      }

      _inherits(Child, _Parent);

      return _createClass(Child, [{ // 寄生继承,将属性属性方法定义到子类原型对象上

          key: "printName",

          value: function printName() {

            conosole.log(this.name);

          }

      }]);

  })(Parent);

可以看到,ES6类继承本质上也是采用寄生组合继承方式

八、总结

截屏2024-04-20 20.44.18.png
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 原型链继承 方法:子构造函数的prototype指向为父构造函数的实例,因为原型链是proto的链表,父构造函数的...
    johe_jianshu阅读 1,031评论 0 1
  • JavaScript的继承 许多面向对象语言都支持两种继承的方式:接口继承和实现继承。接口继承只继承方法签名,而实...
    生软今天解散了吗阅读 3,831评论 0 0
  • 我的理解:继承通俗地讲就是子代拥有了父代的比如:地位,金钱,房产等等。在js中,继承就是让一个对象拥有另一个对象的...
    Anna_Hu阅读 3,490评论 0 1
  • 《JavaScript高级程序设计》提到了6中继承方式:1.原型链继承2.借用构造函数(经典继承)3.组合继承4....
    小泡_08f5阅读 16,704评论 4 11
  • 我的理解:继承通俗地讲就是子代拥有了父代的比如:地位,金钱,房产等等。在js中,继承就是让一个对象拥有另一个对象的...
    sunnyghx阅读 10,557评论 0 3

友情链接更多精彩内容