JavaScript 中的继承主要是依托于原型链的传递来实现的。而原型链又可以简单用玄幻小说中的血脉家族的传承模式来解释。prototype 可以认为是血脉中刻印的属性和能力。
目前 JS 世界的继承类型:
- 原型链继承 -> 胎生血脉,夺舍传承
- 借用构造函数继承 -> 胎生血脉,记忆传承
- 组合继承 -> 胎生血脉,记忆传承 + 老爷爷贴心胎教
- 原型式继承 -> 精血分身,完全克隆自己
- 寄生式继承 -> 精血分身 Plus 版,克隆自己并增加个性化能力
- 寄生组合式继承 -> 精血胎生血脉,可选式胎教
假设有这么一个 Wesleyan 家族,当代的大族长是 John,John 的弟弟是 David。
William 和 Katyusha 是 John 的孩子。
Zeus 是 David 的孩子。
function Father(name, mate) {
this.name = name
this.mate = mate
this.cook = function (){
// Wesleyan 当代家族厨艺精通
console.log(`${this.lastName} 从当代开始传递优秀的家族厨艺`)
}
}
Father.prototype.lastName = 'Wesleyan'
Father.prototype.hunt = function () {
// Wesleyan 血脉祖传的狩猎技巧
console.log(`${this.lastName} 血脉相传的狩猎技巧`)
}
// John 是这代 Wesleyan 家族的大族长,Caitlin 是 John 的伴侣
const John = new Father('John', {
name: 'Caitlin',
sex: 'woman'
})
// David 是 John ,Joe 是 David 的伴侣
const David = new Father('David', {
name: 'Joe',
sex: 'woman'
})
原型链继承
主要是利用修改原型来实现继承效果
缺点:
- 如果有引用属性,将会在所有子类和当前父类中共享改动。
- 子类在实例化时不能够向父类传递参数
function Children(name) {
this.name = name
}
Children.prototype = John
const William = new Children('William')
const Katyusha = new Children('Katyusha')
William.name // William
William.meta.name // Caitlin
William.cook() // 当代父辈手艺不能丢
William.hunt() // 血脉祖传手艺不能丢
Katyusha.name // Katyusha
Katyusha.meta.name // Caitlin
Katyusha.cook() // 当代父辈手艺不能丢
Katyusha.hunt() // 血脉祖传手艺不能丢
// meta 会在 John、William、Katyusha 中共享,也就意味着。。。
// what if ...
William.meta.name = 'Carter'
// then...
William.meta.name // Carter
John.meta.name // Carter
Katyusha.meta.name // Carter
// 这种离大谱的继承关系还是淘汰掉吧。。。
借用构造函数继承
前面有说道过原型链继承会造成离大谱的关系。
那么,如果子类实例化时,仅仅使用借用父类的构造函数呢?
改进:
- 避免引用类型在所有实例中共享
- 可在子类实例化时向父类传递参数
缺点:
- 如果需要继承父类的 prototype 方法,就必须在父类的构造函数中实现,否则将不会继承。
function Children(name, mate, hobby) {
Father.call(this, name, mate)
this.hobby = hobby
}
const William = new Children('William', { name: 'Carter' }, 'football')
const Katyusha = new Children('Katyusha', { name: 'Arthur' }, 'dance')
William.name // William
William.mate.name // William 的伴侣 Carter
William.cook() // William 继承了 Wesleyan 家族的厨艺
William.hobby // William 的爱好 football
William.hunt // undefined William 这辈人遗失了 Wesleyan 家族的狩猎技巧
Katyusha.name // Katyusha
Katyusha.mate.name // Katyusha 的伴侣 Arthur
Katyusha.cook() // Katyusha 继承了 Wesleyan 家族的厨艺
Katyusha.hobby // Katyusha 的爱好 dance
// 此时 John 只能证明自己是 Wesleyan 家族的人,
// 并且是 William、Caitlin 的父辈,并不能证明自己就是 William、Caitlin 父亲
John.name // John
John.mate.name // John 的伴侣 Caitlin
John.cook() // John 有 Wesleyan 家族的厨艺,可能是家族厨艺的源头。
John.hobby // John 的爱好 undefined,Wesleyan 家族早期没啥爱好,吃饱睡
John.hunt() // John 继承了 Wesleyan 家族的狩猎技巧
组合继承
即将原型链继承和借用构造函数继承的组合起来使用,以达到可以继承属性也能继承方法,同时子类还能够拥有自己的属性和方法(引用类型不会在类之间共享)。
改进:
- 可以继承父类的属性和方法。
- 原型上的方法可以复用。
- 可以被 instanceof 和 isPrototypeof 识别
缺点:
- 重复调用了两次父类构造函数 (a. 子类构造函数内一次;b. 改变原型时实例化一次
function Children(name, mate, hobby) {
Father.call(this, name, mate)
this.hobby = hobby
}
// 本应该血脉相传的狩猎技巧,此时需要再次向 John 请求传递。。。
// 正确的做法是需要像下面注释的代码一样重新 new 一个父类的,这里为了方便理解,将 John 再次拿来用用
Children.prototype = John
// Children.prototype = new Father('John', {
// name: 'Caitlin',
// sex: 'woman'
// })
// 修改 prototype 也同时会修改 constructor, 所以需要重新指正
Children.prototype.constructor = Children
const William = new Children('William', { name: 'Carter' }, 'football')
const Katyusha = new Children('Katyusha', { name: 'Arthur' }, 'dance')
William.name // William
William.mate.name // William 的伴侣 Carter
William.cook() // William 继承了 Wesleyan 家族的厨艺
William.hobby // William 的爱好 football
William.hunt() // William 这辈人重新获得了 Wesleyan 家族的狩猎技巧
David 看到 John 又是转世,又是胎教的太麻烦了,那就直接完全克隆自己吧(记忆 + 血脉身体)
原型式继承
基于原有的对象创建子类,将原型指向这个对象,创建者模式。ES5 中的 Object.create 就是原型式继承。
即基于一个对象,创建一个新的对象与其保持类似。等同于复制一个对象,用函数来包装。
改进/优点:
- 所有实例都会继承原型上的属性。
- 快速。
缺点:
- 引用类型会被所有子类共享
- 子类不能使用构造函数初始化属性
- 子类不能够定义自己的方法/属性
// 创造工厂
function creator(obj) {
const Kid = function() {}
Kid.prototype = obj
return new Kid()
}
const Zeus = creator(David)
Zeus.name = 'Zeus'
Zeus.cook() // 当代父辈手艺不能丢
Zeus.hunt() // 血脉祖传手艺不能丢
// David 利用这种技术创造的“后辈”也有 John 的初始方案的弊端,就是引用类型将会被全部共享
// 也就意味着。。。
// what if ...
Zeus.meta.name = 'Carter'
// then...
Zeus.meta.name // Carter
David.meta.name // Carter
// 不愧是你们。。。
完全克隆自己,就意味没有变化和发展,所以必须安排个性化!
寄生式继承
基于原型式继承的改进。工厂模式,创建一个用于封装继承的过程函数,在该函数内部增强对象。
寄生式继承主要是解决了 子类不能有自己的属性和方法,其他的缺陷并没有解决,所以引用的类型问题依旧存在
优点:
- 可以继承 prototype 中的方法和属性,也能继承父类中定义的属性和方法
- 子类可以有自己的方法和属性
缺点:
- 子类不能使用构造函数初始化属性
- 引用类型会被所有子类共享
function creator(obj) {
const Fun = function () {}
Fun.prototype = obj
return new Fun()
}
function creatorChild(orginal) {
const obj = creator(orginal)
// 增强对象
obj.sing = function () {
console.log('sing')
}
return obj
}
const Zeus = creatorChild(David)
Zeus.name = 'Zeus'
Zeus.cook() // 当代父辈手艺不能丢。
Zeus.hunt() // 血脉祖传手艺不能丢。
Zeus.sing() // Zeus 可以有自己的歌唱天赋了,并且也可以继承与后代。
// 引用的类型问题依旧存在,也就意味着。。。
// what if ...
Zeus.meta.name = 'Carter'
// then...
Zeus.meta.name // Carter
David.meta.name // Carter
David 见自己只在血脉继承上手段老道,但来来去去都是“自己人”,那就只克隆血脉吧,记忆就让后辈自己选择是否继承吧。
寄生组合式继承
即寄生式和组合式的再次组合。组合模式的缺点是会重复调用两次父类的构造函数。
改进思路就是当子类的原型不指向父类的实例,而直接指向父类的原型。
引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:
这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
如果再加上 super 的语法糖来实现父类自己的方法和属性继承就是完整的 ES6 中的继承模式了。
function creator(obj) {
const Kid = function() {}
Kid.prototype = obj
return new Kid()
}
// 这只是一个处理过程,所有不需要返回
function prototype(Child, Parent) {
// 以父类的原型创建新的对象
const prototype = creator(Parent.prototype);
// 将构造函数指向于子类
prototype.constructor = Child;
// 改变子类的原型
Child.prototype = prototype;
}
function Children(name) {
this.sing = function () {
console.log('sing')
}
}
prototype(Children, Father)
const Zeus = new Children('Zeus')
Zeus.lastName // Wesleyan
Zeus.hunt() // 血脉祖传手艺不能丢。
Zeus.sing() // Zeus 可以有自己的歌唱天赋了,并且也可以按需继承与后代。
Zeus.name // Children // 没有自己名字,只有创造他的构造器的名字
Zeus.cook // undefined 由于没有向父辈请教,所以 Zeus 没有厨艺,也不会有伴侣。
Zeus.mate // undefined
// 为了让 Zenus 拥有自己的名字,学会厨艺和找到伴侣,父类帮忙实现/教导
function Children2(name, mate) {
Father.call(this, name, mate) // 自己调用,相当于 super
this.sing = function () {
console.log('sing')
}
}
const Zeus = new Children2('Zeus', { name: 'Jan' })
Zeus.lastName // Wesleyan
Zeus.hunt() // 血脉祖传手艺不能丢。
Zeus.sing() // Zeus 可以有自己的歌唱天赋了,并且也可以按需继承与后代。
Zeus.name // Zeus
Zeus.cook() // 继承了父辈的厨艺
Zeus.mate.name // Jan