JavaScript 中的这些继承方式,你弄懂了吗?

detailtoo
发布于 2021-8-9 16:37
浏览
0收藏
面试中我们经常会被问到继承,希望通过此文,你能彻底搞懂 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 上的属性。

JavaScript 中的这些继承方式,你弄懂了吗?-鸿蒙开发者社区

问题

第一:共享问题

当 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 属性,导致了不必要的多余继承。用图表示如下:JavaScript 中的这些继承方式,你弄懂了吗?-鸿蒙开发者社区

原型式继承

 

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 继承有进一步的了解,那将是我最大的欣慰。如果你觉得能学到一点东西的话,还请动动你可爱的小指让更多人看到。如果有错误或者有疑问的地方,也欢迎交流讨论。

分类
标签
已于2021-8-9 16:37:56修改
收藏
回复
举报
回复
    相关推荐