如何利用玄幻小说来解释 JS 中的继承

732 阅读7分钟

JavaScript 中的继承主要是依托于原型链的传递来实现的。而原型链又可以简单用玄幻小说中的血脉家族的传承模式来解释。prototype 可以认为是血脉中刻印的属性和能力。

目前 JS 世界的继承类型:

  1. 原型链继承 -> 胎生血脉,夺舍传承
  2. 借用构造函数继承 -> 胎生血脉,记忆传承
  3. 组合继承 -> 胎生血脉,记忆传承 + 老爷爷贴心胎教
  4. 原型式继承 -> 精血分身,完全克隆自己
  5. 寄生式继承 -> 精血分身 Plus 版,克隆自己并增加个性化能力
  6. 寄生组合式继承 -> 精血胎生血脉,可选式胎教

假设有这么一个 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'
})

原型链继承

主要是利用修改原型来实现继承效果

缺点:

  1. 如果有引用属性,将会在所有子类和当前父类中共享改动。
  2. 子类在实例化时不能够向父类传递参数
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

// 这种离大谱的继承关系还是淘汰掉吧。。。

借用构造函数继承

前面有说道过原型链继承会造成离大谱的关系。

那么,如果子类实例化时,仅仅使用借用父类的构造函数呢?

改进:

  1. 避免引用类型在所有实例中共享
  2. 可在子类实例化时向父类传递参数

缺点:

  1. 如果需要继承父类的 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 家族的狩猎技巧

组合继承

即将原型链继承和借用构造函数继承的组合起来使用,以达到可以继承属性也能继承方法,同时子类还能够拥有自己的属性和方法(引用类型不会在类之间共享)。

改进:

  1. 可以继承父类的属性和方法。
  2. 原型上的方法可以复用。
  3. 可以被 instanceof 和 isPrototypeof 识别

缺点:

  1. 重复调用了两次父类构造函数 (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 就是原型式继承。

即基于一个对象,创建一个新的对象与其保持类似。等同于复制一个对象,用函数来包装。

改进/优点:

  1. 所有实例都会继承原型上的属性。
  2. 快速。

缺点:

  1. 引用类型会被所有子类共享
  2. 子类不能使用构造函数初始化属性
  3. 子类不能够定义自己的方法/属性
// 创造工厂
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

// 不愧是你们。。。

完全克隆自己,就意味没有变化和发展,所以必须安排个性化!

寄生式继承

基于原型式继承的改进。工厂模式,创建一个用于封装继承的过程函数,在该函数内部增强对象。

寄生式继承主要是解决了 子类不能有自己的属性和方法,其他的缺陷并没有解决,所以引用的类型问题依旧存在

优点:

  1. 可以继承 prototype 中的方法和属性,也能继承父类中定义的属性和方法
  2. 子类可以有自己的方法和属性

缺点:

  1. 子类不能使用构造函数初始化属性
  2. 引用类型会被所有子类共享
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