记不住的继承方式
都说程序员是这个世界上最懒的人, 能躺着绝不坐着, 全干着复制黏贴的活.
‘什么, 你说这套逻辑之前写过?!?! 速速把代码呈上来!!!’.
最懒的人往往信奉着‘拿来主义’. 若只是简单的复制黏贴, 就会显得没有逼格.
在 JavaScript 中, 重复用到的逻辑我们会用函数包装起来, 在合适且需要的情况下, 调用该函数即可. 而 apply, call, new 等方法也拓宽了函数的使用场景.
除了这种借来的, 我们还有继承来的. 这就是常说的原型继承. 当对象本身没有要查询的属性或方法时, 它会沿着原型链查找, 找到了就会拿来使用. 这种’无’中生有的事, 不妨了解一下.
预备知识
默认情况下, 所有的原型对象都会自动获得一个 constructor (构造函数)属性, 这个属性是一个指向 prototype 属性所在函数的指针. 构造函数的原型 prototype 上 constructor 的初始值是构造函数本身. 即,
1
Function.prototype.constructor === Function // true
由构造函数构造出来的实例本身没有 constructor 属性, 不过可以通过原型链继承这个属性.
1
2
3
4
5
6
7// 以下person的constructor属性继承自Person.prototype
function Person() {}
Person.prototype.constructor === Person // true
let person = new Person();
person.constructor === Person // true
person.hasOwnProperty('constructor') === false // true
person_1.constructor === Person.prototype.constructor // true简单数据类型和复杂数据类型赋值传参的区别.
JavaScript 中变量不可能成为只想另一个变量的引用. 引用指向的是值. 复杂数据类型的引用指向的都是同一个值.它们相互之间没有引用/指向关系. 一旦值发生变化, 指向该值的多个引用将共享这个变化.
new, apply, call 的函数调用模式.
三者的共同点都是都是指定调用函数的 this 值. 这使得同一个函数可以在不同的语境下正确执行. new 更为复杂一些. 可大致模拟为,
1
2
3
4
5function new(constructor, arguments) {
let instance = Object.create(constructor.prototype) // 姑且称之为 new 的特性一
constructor.apply(instance, arguments) // 姑且称之为 new 的特性二
return instance
}很明显, new 的操作中包涵了 apply, call 要做的事. 在此大胆猜测一下, 在实现继承的过程中, 一旦同时出现 new 和 apply 或 call, 就会有重复交集的可能, 这时就需要想想是否有可以改进的地方.
不着痕迹的拿来主义
‘各单位请注意, 下面到我表演地时候了’
‘上道具!’
1 | function Animal(name) { |
想要无中生有, 那是不可能的😏, 所以我们准备了模板 Animal. Animal 有的东西, Leo 也想拥有.
而且 Animal 能用地东西也同样适用于 Leo.
所以, 我们期待 Leo 最终长成这个样子.
1 | function Leo(name) { |
‘就长这副熊样!? 这和简单的复制黏贴有什么区别!? 这和咸鱼又有什么区别!? 说好的逼格呢!?’
观察一下 Leo, Leo 构造函数内部逻辑和 Animal 构造函数的内部逻辑如出一辙. 既然都是一样的, 为什么不能借来用用呢? 改造一下,
1 | function Animal(name) { |
这种在构造函数内部借函数而不借助原型继承的方式被称之为 借用构造函数式继承.
把属性和方法放在构造函数内部的定义, 使得每个构造出来的实例都有自己的属性和方法. 而对一些需要实例间共享的属性或方法却是没辙.
当然了, 我们本来就没打算止步于此. 构造函数内部可以靠借, 那原型上呢? 如何让 Leo 的原型上能和 Animal 的原型保持一致呢?
‘这不是废话么? 我除了会借, 我还会继承啊, 原型继承啊!!!’
关于原型链, 我们已经知道是怎么一回事了(不知道的可参考从Function入手原型链).
原型继承就是通过原型链实现了对象本身没有的属性访问和方法调用. 利用这个特性, 我们可以在原型上做些手脚.
思路一: 可以使得 Leo 的 prototype 直接指向 Animal 的 prototype.
1 | function Animal(name) { |
这里有一点需要注意的, Leo.prototype = Animal.prototype
这种写法就等于完全覆写了 Leo 的原型, Leo.prototype.constructor
将和 Animal.prototype.constructor
保持一致, 这会使得一些等式显得诡异.
不信, 请看:
1 | Leo.prototype.constructor === Animal.prototype.constructor === Animal |
针对这种情况, 我们往往会做一些修正:
1 | // 接上例代码省略 |
即使修正好了, 可是还有个大问题.
那就是, 如果想给 Leo 原型添加属性或方法, 将会影响到 Animal, 进而会影响到所有 Animal 的实例. 毕竟它们的原型之间已经画了等号.
1 | // 接上例代码省略 |
‘我只想偷个懒, 没想过要捣乱啊😲!!!’
为了消除这种影响, 我们需要一个中间纽带过渡. 还好我们知道 new 可以用来修改原型链.
思路二: Leo 的 prototype 指向 Animal 的实例.
1 | function Animal(name) { |
这种在构造函数内部借函数同时又借助原型继承的方式被称之为 组合继承. Leo 换个角度其实长这样:
1 | function Leo(name) { |
在这种继承模式中, Leo 的实例可以有自己的属性和方法, 实例之间又可以通过 prototype 来共享属性和方法却不会影响 Animal, 还可以通过 _proto_
追溯到 Animal.prototype.
一切都很完美👏. 不过还记得文章开始时所说的么
在实现继承的过程中, 一旦同时出现 new 和 apply 或 call, 就会有重复交集的可能, 这时就需要想想是否有可以改进的地方.
Animal 被调用了两次, 第一次是 Leo 构造函数内部作为一个普通函数被调用, 第二次是被作为构造函数构造一个实例充当 Leo 的原型.
Animal 内部定义的属性和方法同时出现在 Leo 的原型和 Leo 的实例上. 实例上有的东西就不会再到原型上查找. 反之, 实例上没有的东西才会到原型上查找. 显然, 有多余的存在.
‘这不是最优解, 我要最好的! 下一个!’
思路三: 既然有重复, 那就去其一呗. 既然 new 比 call 和 apply 厉害, 那就留着 new 吧.
1 | function Animal(name) { |
这种在构造函数内部不借函数只借助原型继承的方式被称之为 原型链继承.
经过这么一折腾, 发现不好的地方有增无减. 实例没了自己的属性和方法了, 连 Animal 构造函数内部定义的属性方法都可以在实例间共享了(思路二也存在这个问题), 而且参数也不给传了.
‘我要的不多, 能轻点折腾不, 心脏不好’
回到 思路二, 那就删了 new 吧.
思路四: 接上 思路二, 删了 new, 那只能在原型上做调整了.
我们从一开始就只是希望 Leo 的 prototype 指向 Animal 的 prototype, 不多不少且不会出现 思路一 的坏影响.
既然不能直接在两者之间画等号, 就造一个过渡纽带呗. 能够关联起原型链的不只有 new, Object.create() 也是可以的.
创建一个 _proto_
指向 Animal.prototype 的对象充当 Leo 的原型不就解决问题了么.
1 | function Animal(name) { |
这种在构造函数内部借函数同时又间接借助原型继承的方式被称之为 寄生组合式继承.
这种模式完美解决了 思路二 的弊端. 算是较为理想的继承模式吧.
‘确认过眼神, 你才我想要的!’
以上还是只是构造函数间的继承, 还有基于已存在对象的继承, 譬如, 原型式继承 和 寄生式继承等.
讲真, 说了辣么多, 我还真没记住 借用构造函数式继承, 组合继承, 原型链继承, 寄生组合式继承, 原型式继承, 寄生式继承等.
‘你没记住这么多模式, 那你都记住什么了’
答曰: 要想很好得继承, 一靠朋友, 二靠拼爹.
‘这孩子是不是傻? 这都什么年代了? 再说了, 就没人告诉你你家里有矿???’
思路五: ES6 引入了 Class(类)这个概念,通过 class 关键字,可以定义类, Class 实质上是 JavaScript 现有的基于原型的继承的语法糖. Class 可以通过extends关键字实现继承. 我们可以对 思路四 来个华丽变身.
1 | class Animal { |
经过这么一处理后行为上和 思路四 基本没什么区别, constructor(){}
充当了之前的构造函数, super()
作为函数调用扮演着 Animal.call(this, name)
的角色(还可以表示父类). 最重要的是 Leo 的 _proto_
也指向了 Animal.
‘矿多基因好, 啧啧啧, 我都快要喜欢上我自己了😏’