JS的6种继承方式

117 阅读4分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

一、原型链继承

说到原型链继承,我们自然会想到构造函数、原型和实例的关系:每个构造函数都有一个prototype属性指向原型对象,原型有一个属性constructor指回构造函数,而实例有一个内部指针__proto__指向原型。

基本思想是:我们将原型指向一个类型的实例,这样就在他的原型链上了,就可以实现继承。

    function Parent (name) {
      this.name = name
      this.hobby = '炒股'
      this.say = function () {
        console.log(`我的名字叫${this.name},我的爱好是${this.hobby},我的存款是${this.deposit}`)
      }
    }
    Parent.prototype.deposit = '2000000'
    const parent = new Parent('张三')

    function Son (name) {
      this.name = name
      this.hobby = '打游戏'
    }
    Son.prototype = new Parent()
    const son = new Son('张小三')
    Son.prototype.say = function() {
      console.log(`我的名字叫${this.name},我的爱好是${this.hobby},我将继承我爸爸的存款${this.deposit}`)
    }

    parent.say()
    son.say()

image.png 缺点:

  • 在原型中包含引用值的时候,原型中包含的引用值会在所有实例间共享。
  • 子类型在实例化时不能给父类型的构造函数传参。

二、盗用构造函数继承

基本思路:在子类构造函数中调用父类构造函数。

function Son (name) {
  Parent.call(this, arguments)
  this.name = name
}
const son = new Son('张小小三')

parent.say()
son.say()

image.png 缺点:

  • 必须在构造函数中定义方法,因此函数不能重用。
  • 只能继承父类的实例属性和方法,不能继承原型属性/方法

由于存在这些问题,盗用构造函数基本上也不能单独使用。

三、组合继承

综合了原型链和盗用构造函数,基本的思路是:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

// 组合继承
function Son (name) {
  Parent.call(this, arguments)
  this.name = name
  this.hobby = '打游戏'
}
Son.prototype = new Parent()
const son = new Son('张哈哈')
Son.prototype.say = function() {
  console.log(`我的名字叫${this.name},我的爱好是${this.hobby},我将继承我爸爸的存款${this.deposit}`)
}
Son.prototype.say1 = function() {
  console.log(`我的名字叫${this.name},我的爱好是${this.hobby},我将继承我爸爸的存款${this.deposit}`)
}

parent.say()
son.say()
son.say1()

image.png 组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

四、原型式继承

基本思路:

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

object()函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object()是对传入的对象执行了一次浅复制。

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

let person = { 
  name: '张三', 
  hobby: ['炒股', '买基金'] 
}

let son = object(person); 
son.name = '张智成'; 
son.hobby.push('买黄金');

console.log(111, son.name, son.hobby)

image.png ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。

let person = { 
  name: '张三', 
  hobby: ['炒股', '买基金'] 
}

let son = Object.create(person); 
son.name = '张智成'; 
son.hobby.push('买黄金');

console.log(111, son.name, son.hobby)

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

五、寄生式继承

思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createAnother(original){
  let clone = object(original); // 通过调用函数创建一个新对象
  clone.say = function() { // 以某种方式增强这个对象
      console.log(`我的名字叫${this.name},我的爱好是${this.hobby}`);
  };
  return clone; // 返回这个对象
}
let person = { 
  name: '张三', 
  hobby: ['炒股', '买基金'] 
}; 
let son = createAnother(person); 
son.say()

image.png

通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

六、寄生式组合继承

寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
function inheritPrototype(Son, Parent) { 
 let prototype = object(Parent.prototype); // 创建对象
 prototype.constructor = Son; // 增强对象 
 Son.prototype = prototype; // 赋值对象
}

function Parent (name) {
  this.name = name
  this.hobby = '炒股'
  this.say = function () {
    console.log(`我的名字叫${this.name},我的爱好是${this.hobby},我的存款是${this.deposit}`)
  }
}
Parent.prototype.deposit = '2000000'

function Son (name) {
  Parent.call(this, arguments)
  this.name = name
  this.hobby = '打游戏'
}
inheritPrototype(Son, Parent)
Son.prototype.sonSay = function() {
  console.log(`我的名字叫${this.name},我的爱好是${this.hobby},我将继承我爸爸的存款${this.deposit}`)
}
const son = new Son('张哈哈')
son.sonSay()

image.png

寄生式组合继承原型链仍然保持不变,因此 instanceof 操作符和isPrototypeOf()方法正常有效,可以算是引用类型继承的最佳模式。