function Person(eyes) {
this.eyes = eyes
this.getEyes = function () {
return this.eyes
}
}
Person.prototype.ReturnEyes = function () {
return this.eyes
}
function YellowRace() {
Person.call(this, 'black')
}
const hjy = new YellowRace()
console.log(hjy.getEyes()) // black
console.log(hjy.ReturnEyes()) // TypeError: hjy.ReturnEyes is not a function
const myNew = function (Fn, ...args) {
const o = {}
o.__proto__ = Fn.prototype
Fn.apply(o, args)
return o
}
function Person(name, age) {
this.name = name
this.age = age
this.getName = function () {
return this.name
}
}
const hjy = myNew(Person, '滑稽鸭', 22)
console.log(hjy.name)
console.log(hjy.age)
console.log(hjy.getName())
实际上,真正的new关键字会做如下几件事情:
创建一个细新的javaScript对象(即 {} )
为步骤1新创建的对象添加属性proto ,将该属性链接至构造函数的原型对象
将this指向这个新对象
执行构造函数内部的代码(例如给新对象添加属性)
如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象。
代码如下:
const myNew = function (Fn, ...args) {
const o = {}
o.__proto__ = Fn.prototype
const res = Fn.apply(o, args)
if (res && typeof res === 'object' || typeof res === 'function') {
return res
}
return o
}
有些小伙伴可能会疑惑最后这个判断是为了什么?因为语言的标准肯定是严格的,需要考虑各种情况下的处理。比如const res = Fn.apply(o, args)这一步,如果构造函数有返回值,并且这个返回值是对象或者函数,那么new的结果就应该取这个返回值,所以才有了这一层判断。
前言
在面向对象编程中,继承是非常实用也非常核心的功能,这一切都基于面向类语言中的类 。然而,
javascript
和面向类的语言不同,它没有类作为蓝图,javascript
中只有对象,但抽象继承思想又是如此重要,于是聪明绝顶的javascript
开发者们就利用javascript
原型链的特性实现了和类继承功能一样的继承方式。何为原型
要想弄清楚原型链,我们得先把原型搞清楚,原型可以理解为是一种设计模式。以下是《你不知道的javascript》对原型的描述:
《javascript高级程序设计》这样描述原型:
我们通过一段代码来理解这两段话:
这是上面这段代码在
chrome
控制台中显示的结果:可以看到,我们先是创建了一个空的构造函数
Person
,然后创建了一个Person
的实例hjy
,hjy
本身是没有挂载任何属性和方法的,但是它有一个[[Prototype]]
内置属性,这个属性是个对象,里面有name、age
属性和getName
函数,定睛一看,这玩意儿可不就是上面写的Person.prototype
对象嘛。事实上,Person.prototype
和hjy
的[[Prototype]]
都指向同一个对象,这个对象对于Person
构造函数而言叫做原型对象 ,对于hjy
实例而言叫做原型。下面一张图直观地展示上述代码中构造函数、实例、原型之间的关系:因此,构造函数、原型和实例的关系是这样的:每个构造函数都有一个原型对象(实例的原型),原型有一个
constructor
属性指回构造函数,而实例有一个内部指针指向原型。 在chrome、firefox、safari
浏览器环境中这个指针就是__proto__
,其他环境下没有访问[[Prototype]]
的标准方式。这其中还有更多细节建议大家阅读《javascript高级程序设计》
原型链
在上述原型的基础上,如果
hjy
的原型是另一个类型的实例呢?于是hjy
的原型本身又有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。这样,实例和原型之间形成了一条长长的链条,这就是原型链。在原型链中,如果在对象上找不到需要的属性或者方法,引擎就会继续在
[[Prototype]]
指向的原型上查找,同理,如果在后者也没有找到需要的东西,引擎就会继续查找它的[[Prototype]]
指向的原型。 上图理解一下:理解继承
以咱们人类为例,咱全地球人都是一个脑袋、双手双脚,很多基本特征都是一样的。但人类也可以细分种类,有黄种人、白种人、黑种人,咱们如果要定义这三种人,无需再说一个脑袋、双手双脚之类的共同特征,黄种人就是在人类的基础上将皮肤变为黄色,白种人皮肤为白色,黑种人为黑色,如果有其他特征就再新增即可,例如蓝眼睛、黄头发等等。
如果用代码封装,咱们就可以将人类定义为基类或者超类,拥有脑袋、手、足等属性,说话、走路等行为。黄种人、白种人、黑种人为子类,自动复制父类的属性和行为到自身,然后在此基础上新增或者重写某些属性和行为,例如黄种人拥有黄皮肤、黑头发。这就是继承的思想。
js中的继承(原型继承)
在其他面向类语言中,继承意味着复制操作,子类是实实在在地将父类的属性和方法复制了过来,但
javascript
中的继承不是这样的。根据原型的特性,js
中继承的本质是一种委托机制,对象可以将需要的属性和方法委托给原型,需要用的时候就去原型上拿,这样多个对象就可以共享一个原型上的属性和方法,这个过程中是没有复制操作的。javascript
中的继承主要还是依靠于原型链,原型处于原型链中时即可以是某个对象的原型也可以是另一个原型的实例,这样就能形成原型之间的继承关系。然而,依托原型链的继承方式是有很多弊病的,我们需要辅以各种操作来消除这些缺点,在这个探索的过程中,出现了很多通过改造原型链继承而实现的继承方式。
js六种继承方式
原型链继承
直接利用原型链特征实现的继承,让构造函数的
prototype
指向另一个构造函数的实例。上述代码中的
Person构造函数
、YellowRace构造函数
、hjy实例
之间的关系如下图:hjy
实例的head
和hand
属性时,由于hjy
本身并没有这两个属性,引擎就会去查找hjy
的原型,还是没有,继续查找hjy
原型的原型,也就是Person原型对象
,结果就找到了。就这样,YellowRace
和Person
之间通过原型链实现了继承关系。但这种继承是有问题的:
hjy
实例时不能传参,也就是YellowRace
构造函数本身不接受参数。针对第二点,我们通过一段代码来看一下:
可以看到,
hjy
只是想给自己的生活增添一点绿色,但是却被laowang
给享受到了,这肯定不是我们想看到的结果。为了解决不能传参以及引用类型属性共享的问题,一种叫盗用构造函数的实现继承的技术应运而生。
盗用构造函数
盗用构造函数也叫作"对象伪装"或者"经典继承",原理就是通过在子类中调用父类构造函数实现上下文的绑定。
上述代码中,
YellowRace
在内部使用call
调用构造函数,这样在创建YellowRace
的实例时,Person
就会在YellowRace
实例的上下文中执行,于是每个YellowRace
实例都会拥有自己的colors
属性,而且这个过程是可以传递参数的,Person.call()
接受的参数最终会赋给YellowRace
的实例。它们之间的关系如下图所示:虽然盗用构造函数解决了原型链继承的两大问题,但是它也有自己的缺点:
YellowRace
构造函数、hjy
和laowang
实例都没有和Person
的原型对象产生联系。针对第二点,我们看一段代码:
可以看到,
hjy
实例能继承Person
构造函数内部的方法getEyes()
,对于Person
原型对象上的方法,hjy
是访问不到的。组合继承
原型链继承和盗用构造函数继承都有各自的缺点,而组合继承综合了前两者的优点,取其精华去其糟粕,得到一种可以将方法定义在原型上以实现重用又可以让每个实例拥有自己的属性的继承方案。
组合继承的原理就是先通过盗用构造函数实现上下文绑定和传参,然后再使用原型链继承的手段将子构造函数的
prototype
指向父构造函数的实例,代码如下:hjy
终于松了口气,自己终于能独享生活的一点"绿",再也不会被老王分享去了。此时
Person
构造函数、YellowRace
构造函数、hjy
和laowang
实例之间的关系如下图:相较于盗用构造函数继承,组合继承额外的将
YellowRace
的原型对象(同时也是hjy
和laowang
实例的原型)指向了Person
的原型对象,这样就集合了原型链继承和盗用构造函数继承的优点。但组合继承还是有一个小小的缺点,那就是在实现的过程中调用了两次
Person
构造函数,有一定程度上的性能浪费。这个缺点在最后的寄生式组合继承可以改善。原型式继承
文章最终给出了一个函数:
其实不难看出,这个函数将原型链继承的核心代码封装成了一个函数,但这个函数有了不同的适用场景:如果你有一个已知的对象,想在它的基础上再创建一个新对象,那么你只需要把已知对象传给
object
函数即可。ES5
新增了一个方法Object.create()
将原型式继承规范化了。相比于上述的object()
方法,Object.create()
可以接受两个参数,第一个参数是作为新对象原型的对象,第二个参数也是个对象,里面放入需要给新对象增加的属性(可选)。第二个参数与Object.defineProperties()
方法的第二个参数是一样的,每个新增的属性都通过自己的属性描述符来描述,以这种方式添加的属性会遮蔽原型上的同名属性。当Object.create()
只传入第一个参数时,功效与上述的object()
方法是相同的。稍微需要注意的是,
object.create()
通过第二个参数新增的属性是直接挂载到新建对象本身,而不是挂载在它的原型上。原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。上述代码中各个对象之间的关系仍然可以用一张图展示:
这种关系和原型链继承中原型与实例之间的关系基本是一致的,不过上图中的
F
构造函数是一个中间函数,在object.create()
执行完后它就随着函数作用域一起被回收了。那最后hjy
的constructor
会指向何处呢?下面分别是浏览器和node
环境下的打印结果:查阅资料得知
chrome
打印的结果是它内置的,不是javascript
语言标准。 具体是个啥玩意儿我也不知道了🤣。既然原型式继承和原型链继承的本质基本一致,那么原型式继承也有一样的缺点:
object()
不能传,但使用Object.create()
是可以传参的。寄生式继承
寄生式继承与原型式继承很接近,它的思想就是在原型式继承的基础上以某种方式增强对象,然后返回这个对象。
这是一个最简单的寄生式继承案例,这个例子基于
hjy
对象返回了一个新的对象laowang
,laowang
拥有hjy
的所有属性和方法,还有一个新方法sayHai()
。可能有的小伙伴就会问了,寄生式继承就只是比原型式继承多挂载一个方法吗?这也太
low
了吧。其实没那么简单,这里只是演示一下挂载一个新的方法来增强新对象,但我们还可以用别的方法呀,比如改变原型的constructor
指向,在下面的寄生式组合继承中就会用到。寄生式组合继承
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路就是使用寄生式继承来继承父类的原型对象,然后将返回的新对象赋值给子类的原型对象。
首先实现寄生式继承的核心逻辑:
这里没有将新建的对象返回出来,而是赋值给了子类的原型对象。
接下来就是改造组合式继承,将第二次调用构造函数的逻辑替换为寄生式继承:
上述寄生式组合继承只调用了一次
Person
造函数,避免了在Person.prototype
上面创建不必要、多余的属性。于此同时,原型链依然保持不变,效率非常之高效。如图,寄生组合式继承与组合式继承中的原型链关系是一样的:
判断构造函数与实例关系
原型与实例的关系可以用两种方式来确定:
instanceof
操作符和isPrototypeOf()
方法。instanceof
instanceof
操作符左侧是一个普通对象,右侧是一个函数。以
o instanceof Foo
为例,instanceof
关键字做的事情是:判断o
的原型链上是否有Foo.prototype
指向的对象。根据
instanceof
的特性,我们可以实现一个自己instanceof
,思路就是递归获取左侧对象的原型,判断其是否和右侧的原型对象相等,这里使用Object.getPrototypeOf()
获取原型:isPrototypeOf()
isPrototypeOf()
不关心构造函数,它只需要一个可以用来判断的对象就行。以Foo.prototype.isPrototypeOf(o)
为例,isPrototypeOf()
做的事情是:判断在a
的原型链中是否出现过Foo.prototype
。new 关键字
在实现各种继承方式的过程中,经常会用到
new
关键字,那么new
关键字起到的作用是什么呢?简单来说,
new
关键字就是绑定了实例与原型的关系,并且在实例的的上下文中调用构造函数。下面就是一个最简版的new
的实现:实际上,真正的
new
关键字会做如下几件事情:javaScript
对象(即 {} )proto
,将该属性链接至构造函数的原型对象this
指向这个新对象代码如下:
有些小伙伴可能会疑惑最后这个判断是为了什么?因为语言的标准肯定是严格的,需要考虑各种情况下的处理。比如
const res = Fn.apply(o, args)
这一步,如果构造函数有返回值,并且这个返回值是对象或者函数,那么new
的结果就应该取这个返回值,所以才有了这一层判断。结语
功力不够,时间来凑。本人还是一个22届即将毕业的非科班本科生,刚开始写的时候感觉无从下笔,每天只能磨一点点,画图也花了不少时间,不过这也是成长的一部分,之前对原型链的概念一直模模糊糊,这个写作探索的过程中对知识的巩固理解非常有帮助。如果看完此文对你有点帮助,还请手下留赞🤣,感谢感谢。
借鉴文章
详解JS原型链与继承 | louis blog (louiszhai.github.io)
《你不知道的javascript》
《javascript高级程序设计》
作者:滑稽鸭 链接:https://juejin.cn/post/7075354546096046087 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。