面试必问的JavaScript的继承
前言
JavaScript里的继承是面试必问的问题,当面试官让我们聊聊JavaScript的继承,我们应该怎么回答呢?难道是直接张嘴就是原型继承吗?不不不,应该直接和面试官聊ES6引入的class继承,如果没有回忆起class继承的具体流程则可以先和面试官聊聊什么是继承,然后边聊边回忆起class继承。在你聊完class继承后,面试官一定会进一步询问其继承的原理,这时那就给面试官手搓一个寄生组合继承。
继承
什么是继承?继承是JavaScript里的一种机制,它允许一个对象获取另外一个对象的属性和方法,通过继承机制可以复用父对象的属性和方法,这样可以减少代码的重复。
原型链继承
通过将子类的原型对象设置为父类的实例对象,实现原型链的连接。通过这样操作后,子类就可以隐式继承父类的属性和方法了。
我们用一个栗子看看吧。
function Parent() {
this.name = 'parent';
this.age = 10;
this.arr = [1, 2, 3]
}
function Child() {
this.type = 'children';
}
Child.prototype = new Parent();
let s1 = new Child()
console.log(s1.name);
创建Parent构造函数和Child构造函数,通过Child.prototype = new Parent()将Child构造函数的原型设置为Parent的实例对象,实现了原型链的连接,然后创建了一个Child实例对象,当其访问name属性时,首先看自己是否拥有该属性,如果没有就通过其__proto__属性向其原型中寻找,最终找到了name属性并且访问后打印出parent。

但是通过原型链继承存在着一个很大的缺点。当我们分别创建Child的实例对象s1和s2,首先通过s1对arr属性添加一个元素,然后我们一起打印看看s1的arr属性值和s2的arr`属性值。

你会发现s2的arr对象也发生了变化。
没错,原型链继承存在的缺点就是它,因为子类继承父类的引用类型属性都是继承了该引用类型属性的引用地址,简单来说就是子类操作的引用类型属性并不是它自己的,而是操作了父类的,虽然子类可以访问继承的引用类型属性,但是不是只属于自己的,而是所有子类共享的,只要一个子类操作了它,所有子类访问的都是被操作后的。
借用构造函数继承
在子类的构造函数中通过调用父类的构造函数并且改变其的this指向,将this指向子类的构造函数。当子类的通过new关键字创建子类实例化对象会将父类中的属性显式继承给子类的实例对象,因此子类通过这样的方法继承到属性和方法都是属于自己的,并不会其他子类实例对象共享,这样就解决了通过原型链继承存在的缺点,但是这个继承方法又存在着别的缺点,那就是无法继承父类型原型上的方法和属性。这不就是拆东墙补西墙嘛。
只看文字不好理解的话,还是用一个栗子聊聊借用构造函数继承的实现方法。
Parent.prototype.getname = function () {
return this.name;
}
function Parent() {
this.name = 'parent';
this.age = 10;
this.arr = [1, 2, 3]
}
function Child() {
Parent.call(this);
this.type = 'children';
}
let s1 = new Child();
let s2 = new Child();
s1.arr.push(4);
console.log(s1.arr);
console.log(s2.arr);
console.log(s1.getname());
可以看到,我们在Child的构造函数中添加了Parent.call(this)代码,这行代码将Parent的构造函数的this指向显式绑定为Child。在通过new关键字创建Child实例的时候会执行以下步骤:
- new会在构造函数中创建一个object对象。
- 通过call方法将this指向object对象
- 将object对象的原型指向构造函数的原型。
- 执行函数中的逻辑代码(相当于往object对象上添加属性)。
- 返回object对象。
let object = {}
object.__proto__ = Child.prototype
Child.call(object)
//Child应该有的属性
this.type = 'children'
//以下是通过Parent.call(this)在Child实例添加Parent的属性和方法
this.name = 'parent';
this.age = 10;
this.arr = [1, 2, 3]
return object

这样Child实例对象就继承到了Parent的属性和的方法。

根据代码的执行结果,可以看出子类通过借用构造函数继承的属性和方法都是属于自己的,其他子类无法访问,但是子类无法访问父类原型上的属性和方法。
组合式继承
在了解完借用构造函数继承和原型链继承的概念后,你会发现它们的优缺点是互补的,没错,你想的没错,只要将它们结合起来就可以更好得继承父类的属性和方法,因此组合式继承就是借用构造函数继承后再通过原型链继承父类的原型上的对象。
Parent.prototype.getname = function () {
return this.name;
}
function Parent() {
this.name = 'parent';
this.age = 10;
this.arr = [1, 2, 3]
}
function Child() {
//借用构造函数继承
Parent.call(this);
this.type = 'children';
}
//原型链继承
Child.prototype = new Parent();
//手动将 Child 类的原型对象的 constructor 属性重新设置为 Child 函数本身
Child.prototype.constructor = Child;
let s1 = new Child();
let s2 = new Child();
s1.arr.push(4);
console.log(s1.arr);
console.log(s2.arr);
console.log(s1.getname());
首先通过显式继承Parent上的属性和方法,然后让将Parent实例对象作为Child的原型,这样就可以继承到Parent原型上的属性和方法。

但是,这种继承的方法好归好,但是还是有点小毛病,还是不够优雅。
在一个对象的原型中会有一个constructor属性用于记录该对象是由那个构造函数创建的。因为我们将Child的原型赋值为了一个实例对象,因此把Child的原型原本拥有的constructor属性给搞没了,所以需要执行Child.prototype.constructor = Child这行代码手动将 Child 类的原型对象的 constructor 属性重新设置为 Child 函数本身。

我们执行了两次Parent的构造函数导致Child实例对象重复继承了父类的属性,这就是这个方法的缺点。你可能觉得这应该不算缺点吧,这简直就是鸡蛋里挑骨头,但是怎么说的原因肯定是还有更好的继承方法,更好的继承方法弥补了它的不足,所以就成为了缺点。
原型链是这样的。

寄生组合式继承
寄生组合式继承是结合寄生继承和组合继承实现的。那我们看看什么是寄生式继承。
寄生式继承
寄生式继承和原型式继承紧密相关,它的核心思想就是封装一个寄生函数。
let parent = {
name: 'parent',
age: 18,
like: [1, 2, 3]
}
//寄生函数
function clone(obj) {
let clone = Object.create(obj)
clone.getLike = function () {
return this.like
}
return clone
}
let child = clone(parent)
在寄生函数中通过Object.create(obj)创建一个原型为函数形参的空对象,并且可以在函数里对这个新对象添加额外的属性和方法,然后将这个新对象返回。
通过寄生式继承可以继承父类及其原型上的属性和方法,还能拥有自己的属性和方法。
寄生组合式继承
其实寄生组合式继承的方法就是弥补了组合式继承调用两次构造函数造成重复继承的缺点。以下例子并没有用寄生式继承,而是通过原型式继承简单说明原理。
Parent.prototype.getname = function () {
return this.name;
}
function Parent() {
this.name = 'parent';
this.age = 10;
this.arr = [1, 2, 3]
}
function Child() {
Parent.call(this);
this.type = 'children';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
let s1 = new Child();
let s2 = new Child();

这样创建的Child实例对象并没有隐式继承Parent。原型链如下:

实现原理是这样的:通过Object.create(Parent.prototype)创建的空对象替代了Child原来的原型,但是不影响原来的原型链。这样就隐式继承到Parent的属性和对象了。
小tips:Object.create() 是 JavaScript 中的一个方法,用于创建一个新对象,并指定该对象的原型对象。它接受一个参数,这个参数可以是一个对象或者 null。
ES6类继承
ES6类继承的底层原理就是寄生组合式继承,是通过封装寄生组合式继承而来的。
使用class关键字可以定义类,通过extends关键字可以实现类的继承。
class Parent {
constructor(name) {
this.name = name
}
sayName() {
console.log(this.name)
}
}
class Child extends Parent {
constructor(name) {
super(name)
this.nickname = 'Jerry'
}
}
let child = new Child('Tom')
console.log(child.name);
在Child类的构造函数中,使用super()来调用父类的构造函数,以初始化从父类继承来的属性。super方法也可以传递参数给父类。
小结
恭喜你已经可以拿捏JavaScript的继承了。

转载自:https://juejin.cn/post/7393209265584177164