上篇文章详细解析了原型、原型链的相关知识点,这篇文章讲的是和原型链有密切关联的继承,它是前端基础中很重要的一个知识点,它对于代码复用来讲很是有用,本篇将详细解析JS中的各类继承方式和优缺点进行,但愿看完本篇文章可以对继承以及相关概念理解的更为透彻。html
call
的相关知识:js基础-面试官想知道你有多理解call,apply,bind?git
维基百科):继承可使得子类具备父类别的各类属性和方法,而不须要再次编写相同的代码。
继承是一个类从另外一个类获取方法和属性的过程。es6
PS:或者是多个类github
记住这个概念,你会发现JS中的继承都是在实现这个目的,差别是它们的实现方式不一样。
复制父类的属性和方法来重写子类原型对象。web
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
对象。返回其余对象会致使获取不到构造函数的实例,很容易所以引发意外的问题!
咱们知道了fatherFn
的this
和prototype
的属性/方法都跟new
期间建立的新对象有关系。面试
若是在父类中返回了其余对象(new
的第四点),其余对象没有父类的this
和prototype
,所以致使原型链继承失败。数组
咱们来测试一下,修改原型链继承中的父类fatherFn
:babel
function fatherFn() { this.some = '父类的this属性'; console.log('new fatherFn 期间生成的对象', this) return [ '数组对象', '函数对象', '日期对象', '正则对象', '等等等', '都不会返回new期间建立的新对象' ] }
PS: 本文中构造调用函数都不能返回其余函数,下文再也不说起该点。app
这种方式很容易在不经意间,清除/覆盖了原型对象原有的属性/方法,不应为了稍微简便一点,而使用这种写法。
有些人在须要在原型对象上建立多个属性和方法,会使用对象字面量的形式来建立:
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()
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
对象下。new
期间建立的新对象,返回该新对象。fatherFn.prototype
没有任何操做,没法继承。该对象的属性为:子类和父类声明的this
属性/方法,它的原型是
PS: 关于call/apply/bind的更多细节,推荐查看个人博客:[js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]](https://juejin.im/post/5d469e...
优势:
this
声明的属性会在全部实例共享的问题。缺点:
this
声明的属性/方法,不能继承父类prototype
上的属性/方法。prototype
,因此每次子类实例化都要执行父类函数,从新声明父类this
里所定义的方法,所以方法没法复用。原理:使用原型链继承(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
声明的属性,实例根据原型链查找规则,每次都会prototype
声明的属性/方法没法继承的问题(借用构造函数的问题)。缺点:
new fatherFn()
和fatherFn.call(this)
),形成必定的性能损耗。this
声明的属性/方法,生成两份的问题。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
)来继承父类this声明的属性/方法 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上的属性/方法泾渭分明。
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
声明的属性/方法,也跟寄生组合式继承同样。
本段摘自 阮一峰-es6入门文档
由于子类没有本身的this对象,因此必须先调用父类的super()方法。
在寄生组合式继承中有一段以下一段修正constructor 指向的代码,不少人对于它的做用以及为何要修正它不太清楚。
son.prototype.constructor = son; // 修正constructor 指向
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基础-函数、对象和原型、原型链的关系
前端进阶积累、公众号、GitHub、wx:OBkoro一、邮箱:obkoro1@foxmail.com
以上2019/9/22
做者:OBKoro1
参考资料:
JS高级程序设计(红宝书)6.3继承