关于继承:JS-原生方法原理探究四实现继承的几种方式以及优缺点对比

98次阅读

共计 4350 个字符,预计需要花费 11 分钟才能阅读完成。

这是 JS 原生办法原理探索系列的第四篇文章。本文会介绍如何实现 JS 中常见的几种继承形式,同时简要介绍它们的优缺点。

实现继承的办法

图源:《JavaScript 外围原理精讲》,侵删

实现继承的办法共有 7 种,这 7 种办法并不是相互独立的,它们之间更像是一种互补或者加强的关系。

  • 原型链继承和借用构造函数继承别离解决了 继承父类办法 继承父类属性 的问题,这两个办法联合就失去了组合继承;
  • 原型式继承的外围相似于浅拷贝一个对象,寄生式继承则在这个过程的根底上为对象增加办法,进行加强
  • 寄生组合式继承联合了寄生式继承和组合式继承,是绝对比拟完满的计划。
  • Class extends 继承是 ES6 的,实质上是寄生组合式继承的一种使用

上面的示例中,SuperType 示意父类,SubType 示意继承父类的子类。

1)原型链继承

function SuperType(){this.names = []
}
SuperType.prototype.getNames = function(){}
function SubType(){this.ages = []
}
SubType.prototype = new SuperTye()
const obj = new SubType()

原型链继承的外围就一句话:用父类实例作为子类原型,这使得子类实例最终能够拜访父类上的属性和其原型上的办法。而它的毛病也很显著:

第一:因为父类构造函数只调用了一次,导致子类的原型都对立指向了这次调用所创立的父类实例,所以子类实例在拜访一些本身没有的援用类型的属性时,实际上拜访的都是那同一个父类实例上的属性。但通常,实例和实例之间应该都有本人的属性正本,不应该共享属性

第二:同样是因为只调用了一次父类构造函数,所以子类无奈向父类传参

2)借用构造函数继承

function SupterTye(names){
    this.names = names
    this.getNames = function(){}
}
function SubType(){SuperType.call(this,[])
    this.ages = []}
const obj = new SubType()

借用构造函数继承也称为经典继承,这里所谓的借用指的是借用父类构造函数,它的外围就是齐全不应用原型,而是在子类构造函数中通过 call 调用父类构造函数,从而增强子类实例 —— 相当于把父类实例上的属性都搬到子类实例这里来。

这种继承办法的长处就在于,它解决了原型链继承的毛病,咱们当初能够往父类传参了,而且每次 new 子类的时候都会从新调用一次父类,这使得子类的所有实例都有本人的属性正本。

属性是没问题了,办法的继承又有了问题。因为父类构造函数是反复调用的,所以每个实例都有本人的办法正本,但问题是,办法并不需要正本,所有实例齐全应该共享同一个办法,所以这里为每个实例反复创立同一个办法,就存在肯定的性能问题。此外,对于父类原型上的办法,子类是无奈继承的,因为这种继承形式并没有应用到原型。

3)组合继承

看起来,原型链继承善于办法继承,而借用构造函数继承善于属性继承,那么能不能取二者之长呢?实际上,联合两者的长处,就是所谓的组合继承了。

function SuperType(names){this.names = names}
SuperType.prototype.getNames = function(){}
function SubType(){SuperType.call(this,[])
    this.ages = []}
SubType.prototype = new SuperType()
const obj = new SubType()

组合继承应用原型链继承的形式去继承办法,应用构造函数继承的形式去继承属性。

PS:组合继承和原型链继承都重写了子类的原型,在重写之前,子类的原型的 constructor 是指向子类的,重写后就不是了,因为子类的原型被代之以一个 new 创立的对象字面量。这里能够通过 SubType.prototype.constructor = SubType 修复 constructor 的指向。

4)原型式继承

原型式继承所做的事件相似于浅拷贝一个对象,再通过自定义的形式加强新对象。它可能不便地实现在不同对象之间共享信息,同时又不须要额定创立构造函数(外部做了解决)。

const obj = {
    name: 'jack',
    friends: [1,2]
}
fucntion createObject(o){function F(){}
    F.prototype = o
    return new F()}
const anotherObj = createObject(obj)
anotherObj.name = 'Tom'
anotherObj.friends = [3,4]

ES5 在标准层面实现了原型式继承,也就是所谓的 Object.create() 办法,下面代码能够改为:

const obj = {
    name: 'jack',
    friends: [1,2]
}
const anotherObj = Object.create(obj)

这个办法所做的事件和 createObject 办法是一样的,它最终会返回一个新对象,而这个新对象的原型是传入的参数(咱们传入的参数个别充当一个原型对象)。而且,当咱们传参 null 的时候,它最终会返回一个没有原型的纯正的对象,也就是所谓的裸对象(naked object)。

5)寄生式继承

寄生式继承在原型式继承的根底上,为新对象减少了办法:

const obj = {
    name: 'jack',
    friends: []}
function createObject(o){
    // 对象浅拷贝
    let anotherObj = Object.create(o)
    // 对象加强
    anotherObj.getFriends = function(){}
    return anotherObj
}
const anotherObj = createObject(obj)

6)寄生组合式继承

寄生组合式继承的呈现是为了解决组合继承存在的一些问题,这种继承基本上是完满的了。

组合继承最大的问题在于,它两次调用了父类构造函数。第一次是在子类构造函数中 call 调用父类构造函数,这个时候实际上曾经使得子类实例领有了父类的属性;第二次是 new 调用父类构造函数并作为子类的原型,这时候又使得子类原型上也有了父类的属性。因而这两次调用带来的开销问题不说,更要害的是呈现了两组反复的属性,这齐全是不必要的。所以,利用寄生组合式继承,咱们能够做到 只调用一次父类构造函数

假如咱们当初有一个父类,而后须要实现一个继承父类的子类。用寄生组合式继承的话,代码如下:

function SuperType(){
    this.name = 'jack'
    this.friends = []}
SuperType.prototype.getFriends = function(){}

function SubType(){
    // 属性继承
    SuperType.call(this)
}
function inherit(sup,sub){sub.prototype = Object.create(sup.prototype)
    sub.prototype.constructor = sub
    // 或者间接
    sub.prototype = Object.create(sup.prototype,{
        constructor: {
            value: sub
            // enumerable 默认为 false
        }
    })
}
// 办法继承
inherit(SuperType,SubType)
const obj = new SubType()

留神几个要点:

  • 属性继承依然是采纳借用构造函数继承的形式,要害是办法继承。这里通过一个 inherit 函数承受父类和子类,让子类继承父类的办法。在具体实现中,咱们不再像原型链继承或者组合继承那样,new 一个父类构造函数作为子类的原型 —— 尽管成果看起来一样,但这是一次多余的、应该防止的父类调用。相同,咱们借鉴了寄生式继承的做法,创立了一个父类原型的正本作为子类的原型。子类原型和父类原型之间其实是通过 __proto__ 分割起来的,因而在通过子类实例拜访相干办法的时候,能够确保是沿着 子类实例 => 子类实例.__proto__ = 子类原型 => 子类原型.__proto__ = 父类原型 这样的原型链查找,最终肯定能够找到父类原型上的办法,因而就实现了办法继承。
  • 寄生组合式继承同样重写了子类原型,所以须要修复 constructor 的指向,指回子类自身。因为 Object.create 自身承受两个参数,第二个参数能够设置其返回对象的属性的个性,所以也能够在传参时顺便修复 constructor 的指向

7)ES6 Class extends 继承

ES6 新增了 Class,实现继承更加简略,只须要应用 extends 即可:

class SuperClass {constructor(){
        this.a = 1
        this.instanceMethod = function(){console.log('实例的办法')
        }
    }
    prototypeMethod(){console.log('类的原型的办法')
    }
    static staticMethod(){console.log('类的静态方法')
    }
}
class SubClass extends SuperClass {constructor(){super()
    }
}
const sub = new SubClass()        
sub.instanceMethod()              // '实例的办法'
sub.prototypeMethod()             // '类的原型的办法'
SubClass.staticMethod()           // '类的静态方法'  

extends 继承的底子其实还是寄生组合式继承,通过 babel 转译能够晓得,它的外围是一个 _inherits 函数:

function _inherits(subClass, superClass) {if (typeof superClass !== "function" && superClass !== null) {throw new TypeError("Super expression must either be null or a function");
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { 
        value: subClass, 
        writable: true, 
        configurable: true 
    }
  });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

对于 ES6 实现继承的原理,感兴趣的能够浏览我的另一篇文章:从 Babel 转译浅谈 ES6 实现继承的原理,这里不再赘述。

正文完
 0