JS基础-全方面掌握继承
前言
上篇文章详细解析了原型、原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来说非常有用,本篇将详细解析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
没有任何操作,无法继承。
该对象的属性为:子类和父类声明的this
属性/方法,它的原型是
PS: 关于call/apply/bind的更多细节,推荐查看我的博客:js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]
借用构造函数继承的优缺点
优点:
- 可以向父类传递参数
- 解决了原型链继承中:父类属性使用
this
声明的属性会在所有实例共享的问题。
缺点:
- 只能继承父类通过
this
声明的属性/方法,不能继承父类prototype
上的属性/方法。 - 父类方法无法复用:因为无法继承父类的
prototype
,所以每次子类实例化都要执行父类函数,重新声明父类this
里所定义的方法,因此方法无法复用。
组合继承(call+new)
原理:使用原型链继承(
new
)将this
和prototype
声明的属性/方法继承至子类的prototype
上,使用借用构造函数来继承父类通过this
声明属性和方法至子类实例的属性上。
function fatherFn(...arr) {
this.some = '父类的this属性';
this.params = arr // 父类的参数
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn() {
fatherFn.call(this, '借用构造继承', '第二次调用'); // 借用构造继承: 继承父类通过this声明属性和方法至子类实例的属性上
this.obkoro1 = '子类的this属性';
}
sonFn.prototype = new fatherFn('原型链继承', '第一次调用'); // 原型链继承: 将`this`和`prototype`声明的属性/方法继承至子类的`prototype`上
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
const sonFnInstance = new sonFn();
console.log('组合继承子类实例', sonFnInstance)
组合继承的子类实例
从图中可以看到fatherFn
通过this
声明的属性/方法,在子类实例的属性上,和其原型上都复制了一份,原因在代码中也有注释:
- 原型链继承: 父类通过
this
和prototype
声明的属性/方法继承至子类的prototype
上。 - 借用构造继承: 父类通过this声明属性和方法继承至子类实例的属性上。
组合继承的优缺点
优点:
完整继承(又不是不能用),解决了:
- 父类通过
this
声明属性/方法被子类实例共享的问题(原型链继承的问题)
每次实例化子类将重新初始化父类通过this
声明的属性,实例根据原型链查找规则,每次都会 - 父类通过
prototype
声明的属性/方法无法继承的问题(借用构造函数的问题)。
缺点:
- 两次调用父类函数(
new fatherFn()
和fatherFn.call(this)
),造成一定的性能损耗。 - 因调用两次父类,导致父类通过
this
声明的属性/方法,生成两份的问题。 - 原型链上下文丢失:子类和父类通过prototype声明的属性/方法都存在于子类的prototype上
原型式继承(Object.create()
)
继承对象原型-Object.create()实现
以下是Object.create()
的模拟实现,使用Object.create()
可以达成同样的效果,基本上现在都是使用Object.create()
来做对象的原型继承。
function cloneObject(obj){
function F(){}
F.prototype = obj; // 将被继承的对象作为空函数的prototype
return new F(); // 返回new期间创建的新对象,此对象的原型为被继承的对象, 通过原型链查找可以拿到被继承对象的属性
}
PS:上面Object.create()
实现原理可以记一下,有些公司可能会让你讲一下它的实现原理。
例子:
let oldObj = { p: 1 };
let newObj = cloneObject(oldObj)
oldObj.p = 2
console.log('oldObj newObj', oldObj, newObj)
原型式继承优缺点:
优点: 兼容性好,最简单的对象继承。
缺点:
- 因为旧对象(
oldObj
)是实例对象(newObj
)的原型,多个实例共享被继承对象的属性,存在篡改的可能。 - 无法传参
寄生式继承(封装继承过程)
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnother(original){
var clone = cloneObject(original); // 继承一个对象 返回新函数
// do something 以某种方式来增强对象
clone.some = function(){}; // 方法
clone.obkoro1 = '封装继承过程'; // 属性
return clone; // 返回这个对象
}
使用场景:专门为对象来做某种固定方式的增强。
寄生组合式继承(call+寄生式封装)
寄生组合式继承原理:
- 使用借用构造函数(
call
)来继承父类this声明的属性/方法 - 通过寄生式封装函数设置父类prototype为子类prototype的原型来继承父类的prototype声明的属性/方法。
function fatherFn(...arr) {
this.some = '父类的this属性';
this.params = arr // 父类的参数
}
fatherFn.prototype.fatherFnSome = '父类原型对象的属性或者方法';
function sonFn() {
fatherFn.call(this, '借用构造继承'); // 核心1 借用构造继承: 继承父类通过this声明属性和方法至子类实例的属性上
this.obkoro1 = '子类的this属性';
}
// 核心2 寄生式继承:封装了son.prototype对象原型式继承father.prototype的过程,并且增强了传入的对象。
function inheritPrototype(son, father) {
const fatherFnPrototype = Object.create(father.prototype); // 原型式继承:浅拷贝father.prototype对象 father.prototype为新对象的原型
son.prototype = fatherFnPrototype; // 设置father.prototype为son.prototype的原型
son.prototype.constructor = son; // 修正constructor 指向
}
inheritPrototype(sonFn, fatherFn)
sonFn.prototype.sonFnSome = '子类原型对象的属性或者方法'
const sonFnInstance = new sonFn();
console.log('寄生组合式继承子类实例', sonFnInstance)
寄生组合式继承子类实例
寄生组合式继承是最成熟的继承方法:
寄生组合式继承是最成熟的继承方法, 也是现在最常用的继承方法,众多JS库采用的继承方案也是它。
寄生组合式继承相对于组合继承有如下优点:
只调用一次父类
fatherFn
构造函数。避免在子类prototype上创建不必要多余的属性。
使用原型式继承父类的prototype,保持了原型链上下文不变。
子类的prototype只有子类通过prototype声明的属性/方法和父类prototype上的属性/方法泾渭分明。
ES6 extends继承:
ES6继承的原理跟寄生组合式继承是一样的。
ES6 extends
核心代码:
这段代码是通过babel在线编译成es5, 用于子类prototype原型式继承父类prototype
的属性/方法。
// 寄生式继承 封装继承过程
function _inherits(son, father) {
// 原型式继承: 设置father.prototype为son.prototype的原型 用于继承father.prototype的属性/方法
son.prototype = Object.create(father && father.prototype);
son.prototype.constructor = son; // 修正constructor 指向
// 将父类设置为子类的原型 用于继承父类的静态属性/方法(father.some)
if (father) {
Object.setPrototypeOf
? Object.setPrototypeOf(son, father)
: son.__proto__ = father;
}
}
另外子类是通过借用构造函数继承(call
)来继承父类通过this
声明的属性/方法,也跟寄生组合式继承一样。
ES5继承与ES6继承的区别:
本段摘自阮一峰-es6入门文档
ES5的继承实质上是先创建子类的实例对象,再将父类的方法添加到this上。
ES6的继承是先创建父类的实例对象this,再用子类的构造函数修改this。
因为子类没有自己的this对象,所以必须先调用父类的super()方法。
扩展:
为什么要修正construct指向?
在寄生组合式继承中有一段如下一段修正constructor 指向的代码,很多人对于它的作用以及为什么要修正它不太清楚。
son.prototype.constructor = son; // 修正constructor 指向
construct的作用
MDN的定义:返回创建实例对象的Object
构造函数的引用。
即返回实例对象的构造函数的引用,例如:
let instance = new sonFn()
instance.constructor // sonFn函数
construct
的应用场景:
当我们只有实例对象没有构造函数的引用时:
某些场景下,我们对实例对象经过多轮导入导出,我们不知道实例是从哪个函数中构造出来或者追踪实例的构造函数,较为艰难。
这个时候就可以通过实例对象的constructor
属性来得到构造函数的引用:
let instance = new sonFn() // 实例化子类
export instance;
// 多轮导入+导出,导致sonFn追踪非常麻烦,或者不想在文件中再引入sonFn
let fn = instance.construct
// do something: new fn() / fn.prototype / fn.length / fn.arguments等等
保持construct
指向的一致性:
因此每次重写函数的prototype都应该修正一下construct
的指向,以保持读取construct
行为的一致性。
小结
继承也是前端的高频面试题,了解本文中继承方法的优缺点,有助于更深刻的理解JS继承机制。除了组合继承和寄生式继承都是由其他方法组合而成的,分块理解会对它们理解的更深刻。
建议多看几遍本文,建个html
文件试试文中的例子,两相结合更佳!
对prototype还不是很理解的同学,可以再看看:JS基础-函数、对象和原型、原型链的关系
觉得我的博客对你有帮助的话,就给我点个Star吧!
前端进阶积累、公众号、GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com
以上2019/9/22
作者:OBKoro1
参考资料:
JS高级程序设计(红宝书)6.3继承
JS基础-全方面掌握继承的更多相关文章
- JS基础-原型链和继承
创建对象的方法 字面量创建 构造函数创建 Object.create() var o1 = {name: 'value'}; var o2 = new Object({name: 'value'}); ...
- JS基础-全局内置对象
对象 JS中有那些内置对象 数据封装类对象 String.Array.Object.Boolean.Number 其他对象 Math.Date.RegExp.Error.Function.Argume ...
- 【 js 基础 】Javascript “继承”
是时候写一写 "继承"了,为什么加引号,因为当你阅读完这篇文章,你会知道,说是 继承 其实是不准确的. 一.类1.传统的面向类的语言中的类:类/继承 描述了一种代码的组织结构形式. ...
- 【 js 基础 】【读书笔记】Javascript “继承”
是时候写一写 “继承”了,为什么加引号,因为当你阅读完这篇文章,你会知道,说是 继承 其实是不准确的. 一.类1.传统的面向类的语言中的类:类/继承 描述了一种代码的组织结构形式.举个例子:“汽车”可 ...
- JS基础-该如何理解原型、原型链?
JS的原型.原型链一直是比较难理解的内容,不少初学者甚至有一定经验的老鸟都不一定能完全说清楚,更多的"很可能"是一知半解,而这部分内容又是JS的核心内容,想要技术进阶的话肯定不能对 ...
- js基础到精通全面教程--JS教程
适合阅读范围:对JavaScript一无所知-离精通只差一步之遥的人 基础知识:HTML JavaScript就这么回事1:基础知识 1 创建脚本块 1: <script language=”J ...
- js基础篇——call/apply、arguments、undefined/null
a.call和apply方法详解 call方法: 语法:call([thisObj[,arg1[, arg2[, [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象 ...
- JS基础--函数与BOM、DOM操作、JS中的事件以及内置对象
前 言 絮叨絮叨 这里是JS基础知识集中讲解的第三篇,也是最后一篇,三篇JS的基础,大多是知识的罗列,并没有涉及更难得东西,干货满满!看完这一篇后,相信许多正在像我一样正处于初级阶段的同学, ...
- AJAX学习前奏----JS基础加强
AJAX学习前奏----JS基础加强 知识概要: 1.js类&属性&方法的定义 2.静态属性与方法 3.构造方法 4.原型的使用 5.Object对象直接加属性和方法 6.JSO ...
随机推荐
- 什么是CWS、WBS、OBS
今天公司进行CMMI资质审核,审核人提到了WBS,以前对这些名词没有太过于注意,后经过审核人的审核对这个名词有了一个大致的了解,并结合项目经验和网上的一些资料,编此文档.不为别人,主要怕自己忘记了. ...
- .net测试篇之Moq行为配置
系列目录 我们前面说过.Moq在创建模拟对象的时候,简单对象赋值默认值,引用对象赋值为null,但是有些时候接口里面还包含另一个接口对象,我们知道Moq是可以模拟一个接口对象的,我们可以通过配置让Mo ...
- 微信小程序, 解析↵换行
获取到json中的数据,通过“\n”转义,此时我们需要通过正则表达式来替换一下 replace(/↵/g, '\n'); 在页面中可以这样 <text class="test-ti ...
- CTPN
1. https://zhuanlan.zhihu.com/p/34757009 (原理) 2. https://www.jianshu.com/p/471bdbd0170d (bi-LSTM)
- java 获取真实ip和根据ip获取ip所在地区
import com.alibaba.fastjson.JSON; import javax.servlet.http.HttpServletRequest; import java.io.ByteA ...
- OCP培训 MySQL OCP认证实战培训【低价送OCP考证名额】
一.OCP培训 MySQL 5.7 OCP认证全套实战培训[低价送OCP考试名额] 课程目标: 风哥为满足想参加MySQL OCP考证的学员,而设计的一套比较全面OCP实战培训课程. 课程涉及MySQ ...
- .NET CORE 怎么样从控制台中读取输入流
.NET CORE 怎么样从控制台中读取输入流 从Console.ReadList/Read 的源码中,可学习到.NET CORE 是怎么样来读取输入流. 也可以学习到是如何使用P/Invoke来调用 ...
- Codeforces 936C
题意略. 思路: 这个题目没做出来是因为缺少一个整体的构造思路. 正确的构造思路是不断地在s中去构造并且扩大t的后缀,构造好的后缀总是放在前面,然后不断地把它往后挤,最后将s构造成t. 比如: 现在在 ...
- CentOS 源码安装MySQL5.7
一.安装方式源码安装,源码包名称mysql-boost-5.7.27.tar.gz,此版本包含boost库,在解压后的boost路径下.安装时,可以-DDOWNLOAD_BOOST=1 -DWITH_ ...
- NLP(十一) 提取文本摘要
gensim.summarization库的函数 gensim.summarization.summarize(text, ratio=0.2, word_count=None, split=Fals ...