理性分析 JavaScript 中的继承
本文将从最简单的例子开始,从零讲解在 JavaScript 中如何实现继承。
小例子
现在有个需求,需要实现 Cat 继承 Animal ,构造函数如下:
function Animal(name){
this.name = name
}
function Cat(name){
this.name = name
}
注:如对继承相关的 prototype、constructor、__proto__、new 等内容不太熟悉,可以先查看这篇文章:理性分析 JavaScript 中的原型
继承
在实现这个需求之前,我们先谈谈继承的意义。继承本质上为了提高代码的复用性。
对于 JavaScript 来说,继承有两个要点:
- 复用父构造函数中的代码
- 复用父原型中的代码
下面的内容将围绕这两个要点展开。
第一版代码
复用父构造函数中的代码,我们可以考虑调用父构造函数并将 this 绑定到子构造函数。
复用父原型中的代码,我们只需改变原型链即可。将子构造函数的原型对象的 __proto__ 属性指向父构造函数的原型对象。
第一版代码如下:
function Animal(name){
this.name = name
}
function Cat(name){
Animal.call(this,name)
}
Cat.prototype.__proto__ = Animal.prototype
检验一下是否继承成功:我们在 Animal 的原型对象上添加 eat 函数。使用 Cat 构造函数生成一个名为 'Tom' 的实例对象 cat 。代码如下:
function Animal(name){
this.name = name
}
function Cat(name){
Animal.call(this,name)
}
Cat.prototype.__proto__ = Animal.prototype
// 添加 eat 函数
Animal.prototype.eat = function(){
console.log('eat')
}
var cat = new Cat('Tom')
// 查看 name 属性是否成功挂载到 cat 对象上
console.log(cat.name) // Tom
// 查看是否能访问到 eat 函数
cat.eat() // eat
// 查看 Animal.prototype 是否位于原型链上
console.log(cat instanceof Animal) // true
// 查看 Cat.prototype 是否位于原型链上
console.log(cat instanceof Cat) //true
经检验,成功复用父构造函数中的代码,并复用父原型对象中的代码,原型链正常。
图示
弊端
__proto__ 属性虽然可以很方便地改变原型链,但是 __proto__ 直到 ES6 才添加到规范中,存在兼容性问题,并且直接使用 __proto__ 来改变原型链非常消耗性能。所以 __proto__ 属性来实现继承并不可取。
第二版代码
针对 __proto__ 属性的弊端,我们考虑使用 new 操作符来替代直接使用 __proto__ 属性来改变原型链。
我们知道实例对象中的 __proto__ 属性指向构造函数的 prototype 属性的。这样我们 Animal 的实例对象赋值给 Cat.prototype 。不就也实现了Cat.prototype.__proto__ = Animal.prototype
语句的功能了吗?
代码如下:
function Animal(name){
this.name = name
}
function Cat(name){
Animal.call(this,name)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
使用这套方案有个问题,就是在将实例对象赋值给 Cat.prototype 的时候,将 Cat.prototype 原有的 constructor 属性覆盖了。实例对象的 constructor 属性向上查询得到的是构造函数 Animal 。所以我们需要矫正一下 Cat.prototype 的 constructor 属性,将其设置为构造函数 Cat 。
图示
优点
兼容性比较好,并且实现较为简单。
弊端
使用 new 操作符带来的弊端是,执行 new 操作符的时候,会执行一次构造函数将构造函数中的属性绑定到这个实例对象。这样就多执行了一次构造函数,将原本属于 Animal 实例对象的属性混到 prototype 中了。
第三版代码
考虑到第二版的弊端,我们使用一个空构造函数来作为中介函数,这样就不会将构造函数中的属性混到 prototype 中,并且减少了多执行一次构造函数带来的性能损耗。
代码如下:
function Animal(name){
this.name = name
}
function Cat(name){
Animal.call(this,name)
}
function Func(){}
Func.prototype = Animal.prototype
Cat.prototype = new Func()
Cat.prototype.constructor = Cat
图示
ES6
使用 ES6 就方便多了。可以使用 extends 关键字实现继承, 复用父原型中的代码。使用 super 关键字来复用父构造函数中的代码。
代码如下:
class Animal {
constructor(name){
this.name = name
}
eat(){
console.log('eat')
}
}
class Cat extends Animal{
constructor(name){
super(name)
}
}
let cat = new Cat('Tom')
console.log(cat.name) // Tom
cat.eat() // eat
相关知识点
参考书籍
- 《JavaScript高级程序设计(第3版)》
- 《Java核心技术 卷Ⅰ(第9版)》
- 小例子
- 继承
- 第一版代码
- 图示
- 弊端
- 第二版代码
- 图示
- 优点
- 弊端
- 第三版代码
- 图示
- ES6
- 第一版代码
- 相关知识点
- 参考书籍