面试中我们经常会被问到继承,希望通过此文,你能彻底搞懂 JavaScript 中的继承原理。
前言
ES6 以前,JavaScript 中的继承不像其它 oo 语言一样,用特定 class 去实现,它是由构造函数和原型去模拟,下面我们会介绍几种常见的继承方法以及对应的优点和不足。
原型链
什么是原型链?
比如我有一个构造函数,这个构造函数的实例有一个内部指针[[Prototype]]指向构造函数的原型,然后这个构造函数的原型又是另一个构造函数的实例,也就是说这个构造函数原型有一个内部指针[[Prototype]]指向另一个构造函数的原型,如此下去,就构成了一条原型链。那用原型链实现继承用代码表示出来就是这样:
function Parent () {
this.name = 'Twittytop';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child () {
this.age = 29;
}
// 继承
Child.prototype = new Parent();
var ins = new Child();
console.log(ins.getName()); // Twittytop
这样原来在 Parent 上的属性都变成了 Child.prototype 上的属性。
问题
第一:共享问题
当 Parent 上包含有引用属性时,就出出现问题,比如:
function Parent () {
this.friends = ['Jack', 'Tom'];
}
function Child () {
this.age = 29;
}
// 继承
Child.prototype = new Parent();
var ins1 = new Child();
ins1.friends.push('Bob');
var ins2 = new Child();
console.log(ins2.friends); // ["Jack", "Tom", "Bob"]
因为继承之后变成了 Child 的原型属性,所以所有 Child 的实例都指向的是同一个 friends,当其中一个实例修改了这个值之后,变化就会反映到所有实例上。
第二: 传参问题
Child 在实例化是没法向 Parent 传参,当 Parent 依赖外部传参时,就会导致问题。
盗用构造函数
function Parent (name) {
this.name = name;
}
Parent.prototype.getName = function () {
return this.name;
}
function Child (name, age) {
// 继承
Parent.call(this, name);
this.age = age;
}
var ins = new Child('Twittytop', 29);
console.log(ins.name); // Twittytop
console.log(ins.getName); // undefined
可以看到,盗用构造函数的优点是能传递参数,问题是它只能继承实例属性,不能继承原型属性。
组合继承
既然原型链和盗用构造函数继承都有各自的缺点,那我们能不能把这两者结合起来呢?这就是组合继承。
function Parent (name) {
this.name = name;
}
Parent.prototype.getName = function () {
return this.name;
}
function Child (name, age) {
// 继承实例属性
Parent.call(this, name);
this.age = age;
}
// 继承原型属性
Child.prototype = new Parent();
var ins = new Child('Twittytop', 29);
console.log(ins.name); // Twittytop
console.log(ins.getName()); // Twittytop
组合继承弥补了原型链和盗用构造函数的不足,能同时继承实例属性和原型属性,但它的缺点是会调用两次父类构造函数。一次是在 Child 构造函数中执行 Parent.call,一次是在实例化 Parent 时。这样就会导致 Child 的不仅自身实例上有 name 属性,原型上也有 name 属性,导致了不必要的多余继承。用图表示如下:
原型式继承
function Parent (name) {
this.name = 'Twittytop';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child (age) {
this.age = age;
}
// 继承原型属性
Child.prototype = Object.create(Parent.prototype);
var ins = new Child(29);
console.log(ins.getName);
原型式继承只继承了原型上的属性,没有继承实例属性,相比原型链继承更干净,它没有把父类的实例属性继承到自身的原型上面,当然,它和原型链一样,也会有引用属性的共享问题。
寄生式继承
寄生式继承是建立在原型式继承基础上的,寄生式继承用代码表达出来是这样:
function inherit (Parent) {
let pro = Object.create(Parent.prototype);
pro.myMethod = function () {};
return pro;
}
它相比原型式继承多了添加一些自己的属性和方法。
寄生式组合继承
寄生式组合继承综合了盗用构造函数和寄生式继承,它使用盗用构造函数继承实例属性,使用寄生式继承继承原型属性。
function inherit (Child, Parent) {
let pro = Object.create(Parent.prototype);
pro.constructor = Child; // 将constructor重新指回Child
Child.prototype = pro;
}
function Parent (name) {
this.name = name;
}
Parent.prototype.getName = function () {
return this.name;
}
function Child (name, age) {
// 继承实例属性
Parent.call(this, name);
this.age = age;
}
// 继承原型属性
inherit(Child, Parent)
var ins = new Child('Twittytop', 29);
console.log(ins.name); // Twittytop
console.log(ins.getName()); // Twittytop
寄生式组合继承吸取了盗用构造函数和寄生式继承的优点,又没有组合继承中调用父类构造函数两次的不足,是ES5 实现继承的最佳模式。
关于 ES6 的继承,这里就不介绍了,它本质是上述继承的语法糖而已。
写在后面
JavaScript 继承独特的地方就是它的原型,如果这篇文章能让你对 JavaScript 继承有进一步的了解,那将是我最大的欣慰。如果你觉得能学到一点东西的话,还请动动你可爱的小指让更多人看到。如果有错误或者有疑问的地方,也欢迎交流讨论。