JS 常见的 6 种继承方式及extends

1. 原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

function Parent1() {
  this.name = 'parent1';
  this.play = [1, 2, 3]
}

function Child1() {
  this.type = 'child2';
}

Child1.prototype = new Parent1();

上面的代码看似没有问题,虽然父类的方法和属性都能够访问,但其实有一个潜在的问题

  var s1 = new Child1();
  var s2 = new Child2();

  s1.play.push(4);
  console.log(s1.play, s2.play);

两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化

下面我们看看能解决原型属性共享问题的第二种方法


2. 构造函数继承(借助 call)

function Parent1() {
  this.name = 'parent1';
}
Parent1.prototype.getName = function () {
  return this.name;
}

function Child1() {
  Parent1.call(this);
  this.type = 'child1'
}

let child = new Child1();
console.log(child);
console.log(child.getName());  // 会报错

这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,父类原型对象中一旦存在父类自己定义的方法,那么子类将无法继承这些方法

只能继承父类的实例属性和方法,不能继承原型属性或者方法


3. 组合继承(前两种组合)

function Parent3() {
  this.name = 'parent3';
  this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
  return this.name;
}

function Child3() {
  // 第二次调用 Parent3()
  Parent3.call(this);
  this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;

var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);

console.log(s3.play, s4.play);
console.log(s3.getName());
console.log(s4.getName());

之前方法一和方法二的问题都得以解决

但是这里又增加了一个新问题:通过注释我们可以看到 Parent3 执行了两次,第一次是改变Child3 的 prototype 的时候,第二次是通过 call 方法调用 Parent3 的时候,那么 Parent3 多构造一次就多进行了一次性能开销,寄生组合式继承可解决


4. 原型式继承

Object.create 方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)

let parent4 = {
  name: "parent4",
  friends: ["p1", "p2", "p3"],
  getName: function () {
    return this.name;
  }
};

let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");

let person5 = Object.create(parent4);
person5.friends.push("lucy");

console.log(person4.name);
console.log(person4.name === person4.getName());
console.log(person5.name);
console.log(person4.friends);
console.log(person5.friends);

通过 Object.create 这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承 getName 的方法

第一个结果“tom”,person4 继承了 parent4 的 name 属性,但是在这个基础上又进行了自定义。

第二个是继承过来的 getName 方法检查自己的 name 是否和属性里面的值一样,答案是 true。

第三个结果,person5 继承了 parent4 的 name 属性,没有进行覆盖,因此输出父对象的属性。

最后两个输出结果是一样的,因为Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

那么关于这种继承方式的缺点也很明显,多个实例的引用类型属性指向相同的内存,存在篡改的可能,接下来我们看一下在这个继承基础上进行优化之后的另一种继承方式——寄生式继承


5. 寄生式继承

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承

虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法

let parent5 = {
  name: "parent5",
  friends: ["p1", "p2", "p3"],
  getName: function () {
    return this.name;
  }
};

function clone(original) {
  let clone = Object.create(original);
  clone.getFriends = function () {
    return this.friends;
  };
  return clone;
}

let person5 = clone(parent5);

console.log(person5.getName());
console.log(person5.getFriends());

可以看到 person5 是通过寄生式继承生成的实例,它不仅仅有 getName 的方法,而且可以看到它最后也拥有了 getFriends 的方法

可以看到,person5 通过 clone 的方法,增加了 getFriends 的方法,从而使 person5 这个普通对象在继承过程中又增加了一个方法,这样的继承方式就是寄生式继承


6. 寄生组合式继承

在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式

function clone(parent, child) {
  // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
  child.prototype = Object.create(parent.prototype);
  child.prototype.constructor = child;
}

function Parent6() {
  this.name = 'parent6';
  this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
  return this.name;
}

function Child6() {
  Parent6.call(this);
  this.friends = 'child5';
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function () {
  return this.friends;
}

let person6 = new Child6();

console.log(person6);
console.log(person6.getName());
console.log(person6.getFriends());

这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销


7. ES6 的 extends 关键字实现逻辑

class Person {
  constructor(name) {
    this.name = name
  }
  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}

class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用 this 之前首先调用 super()。
    super(name)
    this.age = age
  }
}

const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

因为浏览器的兼容性问题,如果遇到不支持 ES6 的浏览器,那么就得利用 babel 这个编译工具,将 ES6 的代码编译成 ES5,让一些不支持新语法的浏览器也能运行

extends 编译之后的代码片段

function _possibleConstructorReturn(self, call) {
  // ...
  return call && (typeof call === 'object' || typeof call === 'function') 
    ? call 
    : self;
}

function _inherits(subClass, superClass) {
  // 这里可以看到
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });

  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

var Parent = function Parent() {
  // 验证是否是 Parent 构造出来的 this
  _classCallCheck(this, Parent);
};

var Child = (function (_Parent) {
  _inherits(Child, _Parent);

  function Child() {
    _classCallCheck(this, Child);
    return _possibleConstructorReturn(
      this, 
      (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments)
    );
  }

  return Child;
}(Parent));

从上面编译完成的源码中可以看到,它采用的也是寄生组合继承方式


8. 总结

通过 Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似

你可能感兴趣的:(JS 常见的 6 种继承方式及extends)