likes
comments
collection
share

忆、悟 JS原型链

作者站长头像
站长
· 阅读数 44

原型与原型链

原型对象与构造函数

首先根据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.

根据这条规范我们可以确定两点

  1. 可以作为构造函数的Function实例(除了箭头函数外的任何函数)都有一个prototype属性,当这个函数被创建时一个普通的对象(原型对象)也被创建并成为该函数prototype属性的初始值(这个对象默认一定有一个初始属性constructor指向函数本身,实际上对于Function来说原型对象也并不是普通对象,后文说明)
  2. 当这个函数作为构造函数时创建的所有实例都会有一个内部插槽[[prototype]],除非特殊指定,否则被创建实例的[[prototype]]的值为它的构造函数的prototype属性值

在此基础上我们再明确以下几点

  1. 这些函数本身作为Function的实例是使用function Function构造函数创建的,它们也会拥有[[prototype]]内部插槽
  2. 原型对象是一个普通的对象(除了Function的原型),而普通对象都是由function Object构造函数创建的

原型链

原型链其实是JS用来实现类继承的手段,Class只是ES6提供的语法糖,底层的实现仍然是原型链 上面提到的原型对象就是所谓的"原型",它只是一个存在于原型链上的普通对象,我们将原型链想象为单向有层级的自顶向下的结构,这个原型对象的属性和方法会在原型链上向下共享,在原型链上位置越靠后的实例能够拿到的公共属性和方法就越多。

当你向一个实例要求某个它不具备的属性或者方法,如果它不在原型链上,它会很直接的和你说undefined

但是当它可以访问原型链时,它就会在原型链上逐层向上索取,原型链上的大佬都很慷慨,它们会共享自己的属性和方法给这个实例,如果没有就去找更高层的大佬,找到最高层还没有,它只能回来跟你说undefined

可以在浏览器中复现这个过程

忆、悟   JS原型链

我们使用Number构造函数创建了一个对象小a,它只有两个内部插槽值,当我们打出访问符向a索取方法时查看浏览器的自动补全

忆、悟   JS原型链

它向你提供了一连串的可调用方法,它显然自身没有这个能力,这些方法都是原型链上的大佬给的 可以使用__proto__或者Object.getPrototypeOf()来逐个查看原型链更上层的原始对象

这里说明一下__proto__,它充当了一个访问器,其实是JS的历史包袱之一,可以访问实例的内部插槽[[prototype]],实例的内部插槽的值是其构造函数的prototype的引用,所以__proto__访问到了构造函数的原型对象,读取时相当于Object.getPrototypeOf()方法,写入时相当于Object.setPrototypeOf()方法

只要搞清楚插槽[[prototype]]和构造函数的可访问属性prototype的区别就很容易理解__proto__

忆、悟   JS原型链

这里打印的NumberObject其实就是Number.prototypeObject.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也就是访问该函数对应的原型对象
  • 粗体的箭头表示调用构造函数并创建了对象实例(绿色)或者函数对象实例(紫色)

忆、悟   JS原型链

这张图是我根据上述所有内容和对原型链的理解画的,讲到这里其实原型链没有什么难以理解的地方,反而逻辑很清晰,但Function.prototype为原型链中的普遍性逻辑即原型对象是普通对象加了个括弧,而且非常有个性,下面讲一下个人的理解。

我们首先直接查一下Function.prototype的成分

忆、悟   JS原型链

可以看到这是一个函数,大家可以试一下去查其他构造函数的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
评论
请登录