几种JS里常见的继承(原型链继承、构造函数继承、组合继承和寄生组合式继承)

657 阅读3分钟

JS里的继承有很多种,此篇文章只举了三种普通常见的继承类型,希望能为读者日常的学习提供一定的帮助。

原型链继承

我们通过一个例子来了解什么是原型链继承

function SuperType() { // 超类
  this.superproerty = true
}
SuperType.prototype.getSuperValue = function () {
  return this.superproerty
}
function SubType() { // 子类
  this.subproperty = false
}
SubType.prototype.getSuperValue = function () {
  return this.subproperty
}
SubType.prototype = new SuperType()
var instance = new SubType()
console.log(instance.getSuperValue()); 

问:最后输出是什么?true 还是 false ?
答案: 输出 true
原因:SubType.prototype = new SuperType()这一行代码用 SuperType 类型的一个实例重写了 SubType 的原型对象,使得SubType 的原型对象找到了 SuperType 的原型对象。

这个例子中, SubType 继承了 SuperTypeSuperType 我们称之为超类,继承的方式就是通过new SuperType()创建 SuperType 实例,且将该实例赋值给 SubType 的原型来实现的。而实现的本质,是重写子类型的原型对象。

原型链:instance -> SubType的原型 -> SuperType的原型 -> Object的原型

这就是原型链继承,让子类的原型等于超类的实例,实现原型链继承。
缺点:

  1. 超类构造函数中定义的引用类型的实例属性,会在子类的原型上变成属性,被所有子类的实例所共享。
  2. 在创建子类型的实例时,不能向超类的构造函数中传递参数。

构造函数继承

构造函数可以很好的解决原型链继承中的缺点 看一个例子

function SuperType() {
  this.colors = ['red', 'green', 'blue']
}
function SubType() {
  SuperType.call(this)
}
var instance = new SubType()
instance.colors.push('pink')
console.log(instance.colors); // [ 'red', 'green', 'blue', 'pink' ]

首先解释一下 call( ) 的用处:

利用call()能够使用属于另一个对象的方法。

我们再看上面的例子,SuperType.call(this)就是将 SuperType 自己的作用域搬到 SubType 里面去了,等同于在 SubType 里面复制了一份 SuperType 的作用域,并不会影响 SuperType 自己的作用域。

function SuperType(color) {
  this.colors = ['red', 'green', 'blue']
  this.color = color
}
function SubType(color) {
  SuperType.call(this, color)
}
var instance = new SubType()
var instance4 = new SubType('yellow')
var instance2 = new SuperType()  // 作用域没有被影响
var instance3 = new SubType()
instance.colors.push('pink')
console.log(instance.colors); // [ 'red', 'green', 'blue', 'pink' ]
console.log(instance2.colors); // [ 'red', 'green', 'blue' ]
console.log(instance2.color); // undefined 实例instance4并未影响超类作用域
console.log(instance3.colors); // [ 'red', 'green', 'blue' ]
console.log(instance4.color); // yellow

可以看到这四个实例中,作用域并不是同一个,且传参不会混淆。这就很好的解决了原型链的两个缺点。 但它也有缺点 缺点:

  1. 子类的实例对象上可能会存在非必要的属性。
  2. 无法继承到超类原型上的属性。

组合继承

原型链继承 + 构造函数继承
直接上例子

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'green', 'blue']
}
SuperType.prototype.sayName = function () {
  console.log(this.name);
}

function SubType(name, age) {
  SuperType.call(this, name)  // 构造函数继承
  this.age = age
}
SubType.prototype = new SuperType()  // 原型链继承,用SuperType类型的一个实例来重写SubType类型的原型对象
SubType.prototype.sayAge = function () {
  console.log(this.age);
}

var instance1 = new SubType('老鱼', 18)
instance1.colors.push('pink')
console.log(instance1.colors); // ['red', 'green', 'blue', 'pink']
instance1.sayName() // 老鱼
instance1.sayAge() // 18

var instance2 = new SubType('虾米', 20)
console.log(instance2.colors); // [ 'red', 'green', 'blue' ]
instance2.sayName() // 虾米
instance2.sayAge() // 20

上述代码将原型链继承和构造函数继承的优点都利用了,使其规避了两种继承的缺点。

注意:SubType.prototype = new SuperType()一定要在SubType.prototype.sayAge = function () {console.log(this.age);}这行代码之前,如果次序反了会导致sayAge()被覆盖,从而导致sayAge()无法调用。

缺点:

  1. 无论在什么情况下,都会调用两次超类构造函数。

寄生组合式继承

结合借用构造函数传递参数和寄生模式实现继承

function inheritPrototype(subType, superType) {
    var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
    // prototype.constructor 原来是指向superType的
    console.log(prototype.constructor); // [Function: SuperType] 
    prototype.constructor = subType; // 修改constructor指向
    subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
    return this.name
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function () {
    return this.age
}