前言
上篇文章详细解析了原型、原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来说非常有用,本篇将详细解析JS中的各种继承方式和优缺点进行,希望看完本篇文章能够对继承以及相关概念理解的更为透彻。
本篇文章需要先理解原型、原型链以及call
的相关知识:
js基础-面试官想知道你有多理解call,apply,bind?
何为继承?
维基百科:继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。
继承是一个类从另一个类获取方法和属性的过程。
PS:或者是多个类
JS实现继承的原理
记住这个概念,你会发现JS中的继承都是在实现这个目的,差异是它们的实现方式不同。
复制父类的属性和方法来重写子类原型对象。
原型链继承(new):
function fatherFn() {
this.some = '父类的this属性';
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
// 子类
function sonFn() {
this.obkoro1 = '子类的this属性';
}
// 核心步骤:重写子类的原型对象
sonFn.prototype = new fatherFn(); // 将fatherFn的实例赋值给sonFn的prototype
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法' // 子类的属性/方法声明在后面,避免被覆盖
// 实例化子类
const sonFnInstance = new sonFn();
console.log('子类的实例:', sonFnInstance);
原型链子类实例
原型链继承获取父类的属性和方法
fatherFn
通过this声明的属性/方法都会绑定在new
期间创建的新对象上。- 新对象的原型是
father.prototype
,通过原型链的属性查找到father.prototype
的属性和方法。
理解new
做了什么:
new在本文出现多次,new也是JS基础中很重要的一块内容,很多知识点会涉及到new,不太理解的要多看几遍。
- 创建一个全新的对象。
- 这个新对象的原型(
__proto__
)指向函数的prototype
对象。 - 执行函数,函数的this会绑定在新创建的对象上。
- 如果函数没有返回其他对象(包括数组、函数、日期对象等),那么会自动返回这个新对象。
- 返回的那个对象为构造函数的实例。
构造调用函数返回其他对象
返回其他对象会导致获取不到构造函数的实例,很容易因此引起意外的问题!
我们知道了fatherFn
的this
和prototype
的属性/方法都跟new
期间创建的新对象有关系。
如果在父类中返回了其他对象(new
的第四点),其他对象没有父类的this
和prototype
,因此导致原型链继承失败。
我们来测试一下,修改原型链继承中的父类fatherFn
:
function fatherFn() {
this.some = '父类的this属性';
console.log('new fatherFn 期间生成的对象', this)
return [ '数组对象', '函数对象', '日期对象', '正则对象', '等等等', '都不会返回new期间创建的新对象' ]
}
PS: 本文中构造调用函数都不能返回其他函数,下文不再提及该点。
不要使用对象字面量的形式创建原型方法:
这种方式很容易在不经意间,清除/覆盖了原型对象原有的属性/方法,不该为了稍微简便一点,而使用这种写法。
有些人在需要在原型对象上创建多个属性和方法,会使用对象字面量的形式来创建:
sonFn.prototype = new fatherFn();
// 子类的prototype被清空后 重新赋值, 导致上一行代码失效
sonFn.prototype = {
sonFnSome: '子类原型对象的属性',
one: function() {},
two: function() {},
three: function() {}
}
还有一种常见的做法,该方式会导致函数原型对象的属性constructor
丢失:
function test() {}
test.prototype = {
...
}
原型链继承的缺点
父类使用
this
声明的属性被所有实例共享原因是:实例化的父类(
sonFn.prototype = new fatherFn()
)是一次性赋值到子类实例的原型(sonFn.prototype
)上,它会将父类通过this
声明的属性也在赋值到sonFn.prototype
上。
值得一提的是:很多博客中说,引用类型的属性被所有实例共享,通常会用数组来举例,实际上数组以及其他父类通过
this
声明的属性也只是通过原型链查找去获取子类实例的原型(sonFn.prototype
)上的值。
- 创建子类实例时,无法向父类构造函数传参,不够灵活。
这种模式父类的属性、方法一开始就是定义好的,无法向父类传参,不够灵活。
sonFn.prototype = new fatherFn()
借用构造函数继承(call)
function fatherFn(...arr) {
this.some = '父类的this属性';
this.params = arr // 父类的参数
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn(fatherParams, ...sonParams) {
fatherFn.call(this, ...fatherParams); // 核心步骤: 将fatherFn的this指向sonFn的this对象上
this.obkoro1 = '子类的this属性';
this.sonParams = sonParams; // 子类的参数
}
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
let fatherParamsArr = ['父类的参数1', '父类的参数2']
let sonParamsArr = ['子类的参数1', '子类的参数2']
const sonFnInstance = new sonFn(fatherParamsArr, ...sonParamsArr); // 实例化子类
console.log('借用构造函数子类实例', sonFnInstance)
借用构造函数继承的子类实例
借用构造函数继承做了什么?
声明类,组织参数等,只是辅助的上下文代码,核心是借用构造函数使用call
做了什么:
一经调用call/apply
它们就会立即执行函数,并在函数执行时改变函数的this
指向
fatherFn.call(this, ...fatherParams);
- 在子类中使用
call
调用父类,fatherFn
将会被立即执行,并且将fatherFn
函数的this指向sonFn
的this
。 - 因为函数执行了,所以
fatherFn
使用this声明的函数都会被声明到sonFn
的this
对象下。 - 实例化子类,this将指向
new
期间创建的新对象,返回该新对象。 - 对
fatherFn.prototype
没有任何