继承是面向对象编程中被频繁讨论的话题,很多面向对象语言都支持两种继承:dfdgg
- 接口继承
继承方法的签名。 - 实现继承
继承方法的实现。
ECMAScript 不支持接口继承,因为 ECMAScript 的函数没有签名。
ECMAScript 只支持实现继承,主要是通过原型链实现的。
ECMAScript 的继承的基础是原型模式。
ECMAScript 在原型机制的基础上,模拟传统面向对象编程语言中的继承的行为,由此发展出多种不同程度的继承方式。
主要参考资料:
- 《JavaScript 高级程序设计(第4版)》- P238(263/931)
继承的基础
ECMAScript 的继承的实现没有语言的“底层”设计的支持,而是在现有的语言基础上开发编程模式来模拟实现继承的效果。
在开发者探索实现 ECMAScript 的继承的过程中,形成了一些实现继承的、基础的编程模式:
- 原型链
- 盗用构造函数
- 原型式继承
- 寄生式继承
原型链
原型链(Prototype Chain) 使实例对象拥有了多层级的原型,是 ECMAScript 的主要继承方式。
搭建原型链:
动态地将构造函数 A 的实例对象 a 作为构造函数 B 的原型。
此后使用构造函数 B 创建的对象 b 的内部特性 [[Prototype]] 指向实例对象 a ,实例对象 a 的内部特性 [[Prototype]] 指向构造函数 A 的原型。
如此就形成了一条简单的原型链:b -> a -> A.prototype,即多层级的原型。
显然,对象可以访问其原型链上所有原型的所有属性。
所有对象都是类型 Object 的实例,所以所有对象的原型链的终点是 Object.prototype 。
图示:
- 搭建原型链。
示例:
- 搭建原型链。
function EntityA() { this.entityAAttr = '1948' } // 构造函数 A EntityA.prototype.attrFromEntityA = '2017' // 构造函数的原型(2 级原型) const extendEntityA = new EntityA() // 实例对象 a (1 级原型) extendEntityA.attrFromExtendEntityA = '2021' function EntityB() { this.entityBAttr = '2035' } // 构造函数 B EntityB.prototype = extendEntityA // 动态地将实例对象 a 作为构造函数 B 的原型 const entityB = new EntityB(), // 实例对象 b prototypeLv1 = Object.getPrototypeOf(entityB), prototypeLv2 = Object.getPrototypeOf(prototypeLv1) console.log('entityB:', entityB) console.log('entityB.entityBAttr:', entityB.entityBAttr) console.log('entityB.entityAAttr:', entityB.entityAAttr) console.log('entityB.attrFromExtendEntityA:', entityB.attrFromExtendEntityA) console.log('entityB.attrFromEntityA:', entityB.attrFromEntityA) console.log('prototype chain:') console.log('chain 0 :', entityB) console.log('chain 1 :', prototypeLv1) console.log('chain 2 :', prototypeLv2) // 输出: // entityB: EntityB {entityBAttr: '2035'} // entityB.entityBAttr: 2035 // entityB.entityAAttr: 1948 // entityB.attrFromExtendEntityA: 2021 // entityB.attrFromEntityA: 2017 // prototype chain: // chain 0 : EntityB {entityBAttr: '2035'} // chain 1 : EntityA {entityAAttr: '1948', attrFromExtendEntityA: '2021'} // chain 2 : {attrFromEntityA: '2017', constructor: ƒ}
使用原型链实现继承的问题:
-
当原型的属性为引用值时,继承该原型的所有对象都将共享一个值,不能满足每个对象都有独立的属性的需求。
-
不能满足向父构造函数传递参数的需求。
盗用构造函数
从构造函数的层面上,可以将实例对象的属性分为:
-
实例属性
构造函数中使用对象 this 为其实例添加的属性。 -
原型属性
构造函数的原型的属性。
为了解决原型包含引用值导致的继承问题,一种叫作盗用构造函数的技术在开发社区流行了起来。
盗用构造函数(Constructor Stealing) 使得构造函数 B 可以在创建实例时将构造函数 A 的实例属性添加到构造函数 B 的实例中。
盗用构造函数:
在构造函数 B 中,以将构造函数 A 的对象 this 指定为自身的对象 this 的形式调用构造函数 A 。
相比于原型链,使用盗用构造函数的构造函数 B 可以向构造函数 A 传递参数。
示例:
-
盗用构造函数。
function EntityA() { this.entityAAttr = '2045' } function EntityB() { EntityA.call(this) // 将构造函数 EntityA 的实例属性添加到构造函数 EntityB 的实例中 this.entityBAttr = '2049' } const entityB = new EntityB() console.log('entityB:', entityB) // 输出: // entityB: EntityB {entityAAttr: '2045', entityBAttr: '2049'}
-
盗用构造函数,向调用的构造函数传参。
function EntityA(attr) { this.entityAAttr = attr } function EntityB() { EntityA.call(this, '2076') // 向调用的构造函数传参 this.entityBAttr = '2091' } const entityB = new EntityB() console.log('entityB:', entityB) // 输出: // entityB: EntityB {entityAAttr: '2076', entityBAttr: '2091'}
原型式继承
2006 年,Douglas Crockford 写了一篇文章“Prototypal Inheritance In JavaScript”介绍了原型式继承。
原型式继承(Prototypal Inheritance) 是一个功能函数,用于创建一个以指定对象为原型的实例对象。
原型式继承:
定义一个 最基本的构造函数 ,将该构造函数的原型指定为某个对象,然后使用该构造函数创建一个实例。
原型式继承是对原型链实现中创建构造函数的实例的操作的进一步抽象扩展。
(个人认为,原型式继承应该叫作原生式继承。)
因为这个编程模式重要的点,在于使用了最基本的(原生)构造函数来创建实例,而不在于改变了最基本的构造函数的原型。
示例:
- 原型式继承。
function prototypalInheritance(prototype) { function Basic() {} // 定义一个最基本的构造函数 Basic.prototype = prototype return new Basic() } // 原型式继承
ES5 将原型式继承规范化为方法 Object.create() 。
示例:
- 方法 Object.create() ,规范化的原型式继承。
const prototypeObj = { prototypeAttr: '1948' }, // 作为原型的对象 definePropertyObj = { instanceAttr: { configurable: true, enumerable: true, writable: true, value: '2017' } }, obj = Object.create(prototypeObj, definePropertyObj) // 方法 Object.create() ,规范化的原型式继承 console.log('obj:', obj) console.log('obj.instanceAttr:', obj.instanceAttr) console.log('obj.prototypeAttr:', obj.prototypeAttr) // 输出: // obj: {instanceAttr: '2017'} // obj.instanceAttr: 2017 // obj.prototypeAttr: 1948
寄生式继承
寄生式继承也是 Douglas Crockford 首倡的一种编程模式。
寄生式继承(Parasitic Inheritance) 是对原型式继承的进一步扩展,使用原型式继承来创建一个实例,并增强该实例对象(即添加 / 修改属性)。
示例:
- 寄生式继承。
function prototypalInheritance(prototype) { function Basic() {} Basic.prototype = prototype return new Basic() } // 原型式继承 function parasiticInheritance(prototype) { const basic = prototypalInheritance(prototype) // 使用原型式继承 basic.attr = '2021' // 添加 / 修改属性 return basic } // 寄生式继承
继承的实现
在前文介绍的继承的基础编程模式的基础上,开发者探索出了两个主要的继承的实现:
- 组合继承
- 寄生式组合继承
组合继承
组合继承 结合了原型链和盗用函数实现了一种不够完善的“伪继承”。
组合继承:
使用原型链继承原型方法,使用盗用函数继承实例属性。
因为原型属性是多个实例共享的,所以一般在原型中定义需要被多个实例共享的方法,而不是定义一般属性。
而实例属性是被添加到每个实例的,所以一般在构造函数中定义每个实例独享的一般属性,而不是定义方法。
示例:
- 组合继承。
function Super(attr) { this.superInstAttr = attr } Super.prototype.superProtoFunc = function() { console.log('execute Super prototype function') } // 定义 2 级原型方法 const extendSuper = new Super() extendSuper.extendSuperFunc = function() { console.log('execute extend Super function') } // 定义 1 级原型方法 function Sub() { Super.call(this, '1948') // 继承实例属性 this.subInstAttr = '2017' } Sub.prototype = extendSuper // 继承原型方法 const sub = new Sub(), subInheritanceLv1 = Object.getPrototypeOf(sub), subInheritanceLv2 = Object.getPrototypeOf(subInheritanceLv1) console.log('sub:', sub) console.log('sub.superInstAttr:', sub.superInstAttr) console.log('sub.subInstAttr:', sub.subInstAttr) sub.superProtoFunc() sub.extendSuperFunc() console.log('sub inheritance Lv1:', subInheritanceLv1) console.log('sub inheritance Lv2:', subInheritanceLv2) // 输出: // sub: Sub {superInstAttr: '1948', subInstAttr: '2017'} // sub.superInstAttr: 1948 // sub.subInstAttr: 2017 // execute Super prototype function // execute extend Super function // sub inheritance Lv1: Super {superInstAttr: undefined, extendSuperFunc: ƒ} // sub inheritance Lv2: {superProtoFunc: ƒ, constructor: ƒ}
组合继承存在的效率问题:
组合继承的父构造函数会被调用两次:
- 调用父构造函数创建父实例。
- 子构造函数调用父构造函数为子实例添加父实例属性。
在第一次调用父构造函数时创建的父实例,被添加了一定会被子实例遮蔽的父构造函数的实例属性,这些在父实例中的父构造函数的实例属性,是没有必要的,白白占用了内存。
从上面的示例的输出 sub inheritance Lv1: Super {superInstAttr: undefined, extendSuperFunc: ƒ}
可以看到父实例 subInheritanceLv1
中存在父构造函数的实例属性 superInstAttr
。
寄生式组合继承
寄生式组合继承 使用寄生式继承来改进组合继承,使用最基本的构造函数替代了父构造函数来创建父实例。
寄生式组合模式可以算是使用 ECMAScript 实现继承的最佳模式。
寄生式组合继承:
使用组合继承来继承实例属性和原型方法。
使用寄生式继承来创建父实例,然后修正父实例的属性 constructor ,彻底地重新组合了原型模式“原型 - 构造函数 - 实例”中的“原型 - 构造函数”。
示例:
- 寄生式组合继承。
function prototypalInheritance(prototype) { function Basic() {} Basic.prototype = prototype return new Basic() } // 原型式继承 function inheritPrototype(subType, superType) { const extendSuper = prototypalInheritance(superType.prototype) // 使用原型式继承创建父实例 extendSuper.constructor = subType; // 修正父实例(1 级原型)的属性 constructor extendSuper.extendSuperFunc = function() { console.log('execute extend Super function') } subType.prototype = extendSuper } // 继承原型(寄生式组合继承的核心) function Super(attr) { this.superInstAttr = attr } // 父构造函数 Super.prototype.superProtoFunc = function() { console.log('execute Super prototype function') } function Sub() { Super.call(this, '1948') // 继承实例属性 this.subInstAttr = '2017' } // 子构造函数 inheritPrototype(Sub, Super) // 继承原型方法 // 完成寄生式组合继承 const sub = new Sub(), subInheritanceLv1 = Object.getPrototypeOf(sub), subInheritanceLv2 = Object.getPrototypeOf(subInheritanceLv1) console.log('sub:', sub) console.log('sub.superInstAttr:', sub.superInstAttr) console.log('sub.subInstAttr:', sub.subInstAttr) sub.superProtoFunc() sub.extendSuperFunc() console.log('sub inheritance Lv1:', subInheritanceLv1) console.log('sub inheritance Lv2:', subInheritanceLv2) // 输出: // sub: Sub {superInstAttr: '1948', subInstAttr: '2017'} // sub.superInstAttr: 1948 // sub.subInstAttr: 2017 // execute Super prototype function // execute extend Super function // sub inheritance Lv1: Super {constructor: ƒ, extendSuperFunc: ƒ} // sub inheritance Lv2: {superProtoFunc: ƒ, constructor: ƒ}
从上面的示例的输出 sub inheritance Lv1: Super {constructor: ƒ, extendSuperFunc: ƒ}
可以看到父实例 subInheritanceLv1
中不存在父构造函数的实例属性 superInstAttr
。
寄生式组合继承与组合继承的区别:
- 在继承父构造函数的实例属性上两者没有本质区别。
- 在继承父构造函数的原型属性上两者有区别:
-
组合继承:
直接使用父构造函数来创建父实例。
导致父实例中包含没有必要存在的父构造函数的实例属性。
-
寄生式组合继承:
使用 最基本的构造函数 来创建父实例。
其父实例中没有包含父构造函数的实例属性。
使用最基本的构造函数创建的父实例,本质上只是一个原型为父构造函数的原型的 Object 实例,是携带的数据最少的对象,其携带的数据量等于 Object 实例的数据量。
-
目前为止,使用寄生式组合继承完成了在 ECMAScript 中实现实现继承的目标,即实现了两个目标:
- 实现子实例继承父构造函数的实例属性。
- 实现子实例继承父构造函数的原型方法。