忆、悟 JS原型链
原型与原型链
原型对象与构造函数
首先根据ES规范来解释原型对象和构造函数
Function instances that can be used as a constructor have a "prototype" property. Whenever such a Function instance is created another ordinary object is also created and is the initial value of the function's "prototype" property. Unless otherwise specified, the value of the "prototype" property is used to initialize the [[Prototype]] internal slot of the object created when that function is invoked as a constructor.
根据这条规范我们可以确定两点
- 可以作为构造函数的
Function
实例(除了箭头函数外的任何函数)都有一个prototype
属性,当这个函数被创建时一个普通的对象(原型对象)也被创建并成为该函数prototype
属性的初始值(这个对象默认一定有一个初始属性constructor
指向函数本身,实际上对于Function
来说原型对象也并不是普通对象,后文说明) - 当这个函数作为构造函数时创建的所有实例都会有一个内部插槽
[[prototype]]
,除非特殊指定,否则被创建实例的[[prototype]]
的值为它的构造函数的prototype
属性值
在此基础上我们再明确以下几点
- 这些函数本身作为
Function
的实例是使用function Function
构造函数创建的,它们也会拥有[[prototype]]
内部插槽 - 原型对象是一个普通的对象(除了
Function
的原型),而普通对象都是由function Object
构造函数创建的
原型链
原型链其实是JS用来实现类继承的手段,Class
只是ES6提供的语法糖,底层的实现仍然是原型链
上面提到的原型对象就是所谓的"原型",它只是一个存在于原型链上的普通对象,我们将原型链想象为单向有层级的自顶向下的结构,这个原型对象的属性和方法会在原型链上向下共享,在原型链上位置越靠后的实例能够拿到的公共属性和方法就越多。
当你向一个实例要求某个它不具备的属性或者方法,如果它不在原型链上,它会很直接的和你说undefined
但是当它可以访问原型链时,它就会在原型链上逐层向上索取,原型链上的大佬都很慷慨,它们会共享自己的属性和方法给这个实例,如果没有就去找更高层的大佬,找到最高层还没有,它只能回来跟你说undefined
可以在浏览器中复现这个过程
我们使用Number
构造函数创建了一个对象小a
,它只有两个内部插槽值,当我们打出访问符向a
索取方法时查看浏览器的自动补全
它向你提供了一连串的可调用方法,它显然自身没有这个能力,这些方法都是原型链上的大佬给的
可以使用__proto__
或者Object.getPrototypeOf()
来逐个查看原型链更上层的原始对象
这里说明一下__proto__
,它充当了一个访问器,其实是JS的历史包袱之一,可以访问实例的内部插槽[[prototype]]
,实例的内部插槽的值是其构造函数的prototype
的引用,所以__proto__
访问到了构造函数的原型对象,读取时相当于Object.getPrototypeOf()
方法,写入时相当于Object.setPrototypeOf()
方法
只要搞清楚插槽[[prototype]]
和构造函数的可访问属性prototype
的区别就很容易理解__proto__
。
这里打印的Number
和Object
其实就是Number.prototype
和Object.prototype
原型对象,结合前面所说,这个对象的constructor
属性指向函数本身,Number.prototype.constructor
就是function Number
。
之前也提到普通对象一定是被function Object
构造函数创建的,所以Number.prototype
会有一个内部插槽[[Prototype]]
,a._proto_.__proto__
就访问了Number.prototype
的插槽也就是function Object
对应的原型对象Object.prototype
,同时你可能也发现了Object.prototype.__proto__
直接被写成了null
结合前面的第4条一般构造函数对应的原型对象prototype
是一个普通对象,这些构造函数的实例通过原型链将会访问到这个对象,而这个对象进一步向上访问就会到达function Object
的原型对象Object.prototype
,原型链的顶层其实就是这个Object.prototype
原型对象。
我们可以通过这些关系画出下面这张图,其中
- 黄色矩形表示普通对象
- 紫色圆角矩形表示函数对象(JS中函数也是对象,且不应把函数对象看成普通对象,这也是能否理解原型链的关键之处)
- 红色圆角矩形也是函数对象,但是被当作原型对象,较特殊需要单独说明
- 黑色实线箭头表示调用
__proto__
访问器来访问插槽[[prototype]]
- 黑色虚线箭头表示访问函数对象属性
prototype
也就是访问该函数对应的原型对象 - 粗体的箭头表示调用构造函数并创建了对象实例(绿色)或者函数对象实例(紫色)
这张图是我根据上述所有内容和对原型链的理解画的,讲到这里其实原型链没有什么难以理解的地方,反而逻辑很清晰,但Function.prototype
为原型链中的普遍性逻辑即原型对象是普通对象加了个括弧,而且非常有个性,下面讲一下个人的理解。
我们首先直接查一下Function.prototype
的成分
可以看到这是一个函数,大家可以试一下去查其他构造函数的prototype
的类型成分,不管是内建函数还是自建函数都是Object
,除了Function
所有构造函数的原型对象都遵循了一致的逻辑,但Function
的独特之处还不止于此
可以把下面的代码粘贴到控制台运行
console.log(Function.prototype === Function.__proto__)
不知道大家是怎么理解这个true
的
Function
自己也是Function
的实例?我觉得虽然JS因为各种历史原因会有一些奇怪之处但不至于此。
我更倾向于Function.prototype
就是一个比较特殊的内建函数对象,它充当了Function
的原型对象,Function
是一切函数的构造函数,作为一等公民它不愿意拥有一个高层原型干脆指向了自己的原型。
但这不影响其他函数的逻辑,它们的构造函数是Function
,它们会通过访问器__proto__
访问到Function.prototype
,而为了使原型链完整,Function.prototype.__proto__
指向了顶层原型Object.prototype
,这样函数和对象的原型链终点都是一致的。
总结
看到这里,原型链其实无外乎几种逻辑。
- 凡是构造函数(比如
Number
)一定有对应的原型对象(Number.prototype
) - 被构造函数创造的实例一定有一个内部插槽
[[prototype]]
指向构造函数的原型对象,这个插槽只能被访问器属性__proto__
或者Object.get/setPrototypeOf()
方法访问 - 构造函数本身也是函数对象,它们是
Function
的实例,Number.__proto__
会访问到Function.prototype
- 构造函数的原型对象也就是其
prototype
属性一般是一个普通对象,是function Object
的实例,Number.prototype.__proto__
会访问到Object.prototype
Function
比较特殊:Function
的__proto__
访问器会得到自己的原型Function.prototype
Function
的原型Function.prototype
是一个函数对象而不是普通对象Function.prototype.__proto__
会得到Object.prototype
- 自建函数和自建对象遵循与这个例子一致的逻辑,可以将他们代入图中相应的位置
- 原型链和原型是可以改变的,但是一般不会去修改内建的部分。
- 原型链的顶层是
Object.prototype
,Object.prototype.__proto__ === null
- 顺带一提,
function Object
当然也是Function
的实例
这篇博客到这里就结束了,希望能帮助你来理解原型链,个人水平有限所以标题说可能悟得透彻
当然这些内容都是我个人理解,可能有谬误,请路过大佬务必指出。
转载自:https://juejin.cn/post/7242145395723288633