携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 29 天,点击查看活动详情
问题描述
继承
是基于面向对象的,使用 继承
可以让我们更好的复用以前开发的代码,缩短开发的周期、提升开发的效率。 继承
在各种语言中都充当着至关重要的角色,尤其是在 JavaScript
中,它天生的灵活性,使运用场景更加的丰富。 JavaScript
中的继承也经常用于一些组件库底层的搭建,在 JavaScript
的学习中也尤为重要。今天我们就一起来盘点一下 JavaScript
中常见的继承方式吧!
首先我们先来思考几个问题,带着问题来学习,这样可以加深我们学习的记忆。
问题1:JS
中的继承到底有多少种实现方式呢?
问题2:ES6
中的 extends 关键字是用哪种继承方式实现的呢?
关于继承的探究
继承
简单来说,就是有一个父类,生活中常见的案例就是汽车,它包含颜色、轮胎、品牌、速度、排气量等等,然后基于汽车这个父类,它又可以生产出轿车、货车、大巴车,这些车型都是基于汽车这个父类生成的。总的来说, 继承
可以使子类具有父类的各种属性和方法,下面我们就一起来学习一下 JavaScript
中的几种继承方式。
原型链继承
原型链继承 是比较常见的继承方式之一,其中涉及的构造函数、原型和实例三者之间存在着紧密的联系,也就是每个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。下面我们一起通过代码来了解一下,如下:
function Parent() { this.name = 'parent'; this.list = [1, 2, 3]; } function Child() { this.type = 'child'; } Child.prototype = new Parent(); console.log(new Child());
上述的代码中,看起来没有什么问题,都可以正常的执行,但是父类和子类都有一个潜在的问题,那就是它们都指向同一个原型对象,内存空间是共享的,当一个对象发生改变时,另外一个对象也会随之改变,这就是使用原型链继承的一个缺点。为了解决这个问题,我们就需要继续学习其它的继承方式。
构造函数继承
在前面的 原型链继承
中,我们知道了它的缺陷,那么有什么办法可以规避这个缺陷呢?答案是使用 构造函数继承
,我们还是使用之前的代码,通过修改相关的内容来解决这个问题,代码如下:
function Parent() { this.name = 'parent'; this.list = [1, 2, 3]; } Parent.prototype.getName = function () { return this.name; }; function Child() { Parent.call(this); this.type = 'child'; } const child = new Child(); console.log(child); // 正常运行 console.log(child.getName()); // 会报错,因为子类中不能直接访问父类原型中的方法
上述的代码中,虽然子类可以继承父类的属性,但是当父类原型中一旦存在相关的原型方法,子类就无法继承这些方法,因此在子类中就无法使用父类的getName
。构造函数继承
能使父类的引用属性不会被共享,优化了 原型链继承
中的弊端,但随之而来的缺点也比较明显,就是子类不能继承父类原型中的属性和方法,针对上面这两种继承的方式,又产生了第三种继承方式,这种继承方式结合了前两种继承方式的优缺点。
组合继承
原型链继承
和 构造函数继承
它们的优缺点都非常明显,因此基于这两种继承就出现了第三种继承 -- 组合继承。它主要是为了解决父类引用属性被共享,以及子类无法继承父类原型中属性和方法的问题,下面我们一起来看代码,如下:
function Parent() { this.name = 'parent'; this.list = [1, 2, 3]; } Parent.prototype.getName = function () { return this.name; }; function Child() { Parent.call(this); this.type = 'child'; } Child.prototype = new Parent(); // 原型对象指向自己的构造函数 Child.prototype.constructor = Child; const child = new Child(); const child2 = new Child(); child.list.push(4); console.log(child.list, child2.list); // 互不影响 console.log(child.getName()); // 正常输出 parent console.log(child2.getName()); // 正常输出 parent
执行上述代码后,可以在控制台中看到它们的执行结果,我们发现在 原型链继承
和 构造函数继承
中存在的问题都已经解决了,但是这里又产生了一个新的问题,通过上述代码,我们可以明显发现 Parent
执行了两次,第一次是将子类原型指向父类的的时候,第二次这是在子类中通过 call
调用父类。当父类每多执行一次的时候,就会多产生一份性能的开销。那么是否有更好的办法来解决这个问题呢?让我们接着来看下一个继承方式。
原型式继承
说到 原型式继承 就不得不提 ES5
中的 Object.create
方法,这个方法接收两个参数。第一个参数是用作于新对象原型的对象,第二个参数是可选参数,主要是给新对象定义额外属性的对象,让我们一起来看一段代码,看一下普通对象是如何实现继承的,代码如下:
const parent = { namn: 'parent', children: ['zhangsan', 'lisi'], getName: function () { return this.name; } }; let person = Object.create(parent); person.name = 'son'; person.children.push('wangwu'); let person2 = Object.create(parent); person2.children.push('zhaoliu'); console.log(person.name); console.log(person.name === person.getName()); console.log(person2.name); console.log(person.children); console.log(person2.children);
在上述代码中,我们可以看到普通对象中子类通过 Object.create
实现继承,不仅能够继承父类的属性,也能继承父类的方法,但是这种实现方式也有缺点,那就是多个实例对象的引用,类型属性指向相同的内存地址,存在数据被篡改的可能,因此基于这种实现方式,我们可以得到一个新的继承方法 -- 寄生式继承 。
寄生式继承
使用 原型式继承
可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些相关的方法,这样的继承方法叫做 寄生式继承。虽然优缺点跟 原型式继承
一样,但对于普通对象来说, 寄生式继承
相比于 原型式继承
还是在父类的基础上添加了更多的方法,下面我们一起看一下代码是怎么实现的,如下:
const parent = { name: 'parent', children: ['zhangsan', 'lisi'], getName: function () { return this.name; } }; function clone (targetObj) { let clone = Object.create(targetObj); clone.getChildren = function () { return this.children; }; return clone; } const person = clone(parent); console.log(person.getName()); console.log(person.getChildren());
通过上述的代码,我们可以看到 person
是通过 寄生式继承
实现的,生成的实例不仅有 getName
方法,我们自己添加的 getChildren
方法也被继承到子类,并能够正确执行。在前面第三种继承中的实现方式造成父类被两次调用造成浪费,通过 寄生式继承
就能很好的解决这个问题。结合前面这几种继承方法,我们继续学习最后一种最优的继承方式 -- 寄生组合式继承 。
寄生组合式继承
基于前面几种继承方式的优缺点,我们得出了寄生组合式
的继承方式,这也是所有继承方式里面相对最优的继承方式,让我们一起通过代码来学习一下,代码如下:
function clone (target, source) { source.prototype = Object.create(target.prototype); source.prototype.constructor = source; } function Parent () { this.name = 'parent'; this.children = ['zhangsan', 'lisi']; } Parent.prototype.getName = function () { return this.name; }; function Child () { Parent.call(this); this.children = 'wangwu'; } clone(Parent, Child); Child.prototype.getChildren = function () { return this.children; }; const person = new Child(); console.log(person); console.log(person.getName()); console.log(person.getChildren());
通过上述代码,我们发现 寄生组合式继承
能够解决前几种继承方式的缺点,较好的实现了继承想要的结果,同时也减少了构造函数的次数,并减少了性能的开销。
ES6 - extends
我们使用 es6
中的 extends
关键词很容易就能实现 JavaScript
的继承,但是如果想要深入了解 extends
语法糖是怎么实现的,就得深入研究 extends
的底层逻辑,我们先看一下使用 extends
是怎么实现继承的,代码如下:
class Person { constructor (name) { this.name = name; } getName() { return this.name; } } class Child extends Person { constructor (name, age) { super(name); this.age = age; } } const child = new Child('zhangsan', 18); console.log(child.getName());
上述的代码通过 babel
转换后,最终的实现方法还是基于我们前面的 寄生组合式继承
,因此也证明了 寄生组合式继承
是目前最优的解决方式。
最后
我们可以回答一下前面的两个问题了。
第一个问题:JS
中的继承到底有多少种实现方式呢?答:总共是6种,分别是 原型链继承
、构造函数继承
、组合继承
、原型式继承
、寄生式继承
、寄生组合式继承
。
第二个问题:ES6
中的 extends 关键字是用哪种继承方式实现的呢?答:extends
是通过 寄生组合式继承
实现的,它只是一种 es6
中的语法糖。
通过对 JavaScript
继承方式的探讨,不仅能够让我们加深对 js 的理解,也能为我们后续去学习其它内容打下坚实的基础。
总结给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
2、前端技术导航大全 推荐:★★★★★
地址:前端技术导航大全
3、开发者颜色值转换工具 推荐:★★★★★
地址 :开发者颜色值转换工具