共计 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 实现继承的原理,这里不再赘述。