在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


JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师