全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-11-09_从 ECMAScript 认识 JS(章二):跟原型链比划比划

您的位置:首页 >> 新闻 >> 行业资讯

从 ECMAScript 认识 JS(章二):跟原型链比划比划 本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! 大家好,我是早晚会起风。 这是专栏《从 ECMAScript 语言规范和浏览器引擎的视角认识 JavaScript》的第三篇文章。 强烈建议按照文章顺序阅读,点击上方专栏可以查看所有文章。 前言在上一章中,我们讲了一大堆基础概念,它们是理解 ECMAScript 规范必不可少的部分。在阅读本篇文章之前,强烈建议读者先阅读上一章。 这篇文章,我们来点实际的,从规范的角度精确理解 JavaScript 原型链。对于原型链,已经有很多经典书籍以及数不胜数的文章来解释了。但绝大部分解释都是在进行总结归纳,在理解过程中我们总会产生一些疑问,比如 JavaScript 内部对象属性的查找规则真的和总结归纳的一致吗,还是这只是一种用于理解原型链的心智模型?原型链就像一个黑盒,我们始终看不到在它内部发生的事情。 今天,我们就来从根本上解决这个问题。文章中会涉及到规范中的一些专有词汇,为了表达的准确性,我可能会使用英文词汇而不是中文词汇,不用感到畏惧,它们理解起来都很简单。 一个例子首先我们先举一个例子,来看看我们是怎样查找一个对象的属性。 constfoo={name1:'grace'} constbar={name2:'walk'} Object.setPrototypeOf(foo,bar) foo.name2//walk 这里我们定义了对象foo,并将它的原型设置为另一个对象bar。这样我们就可以通过foo.name2来拿到 bar 对象上 name2 的值。 在我们的理解中,当查找对象上的属性时,JavaScript 会沿着原型链一直向上查找,直到找到对应的属性,或者一直到原型链顶端(null)也没找到该属性,返回 undefined。 那么 JavaScript 真是这样做的吗?在这个过程中发生了什么? 对象属性查找过程essential internal method —— [[Get]]对象上查找属性的方法在规范中定义在essential internal methods中,规范定义如下, theessential internal methodsused by this specification that are applicable to all objects created or manipulated by ECMAScript code. Every object must have algorithms for all of the essential internal methods. However, all objects do not necessarily use the same algorithms for those methods. essential internal methods 直译过来叫做基础内部方法,它用于描述 JavaScript 中对象的一些行为。每一个对象,不论是ordinary object还是exotic object都有这些方法对应的实现,但是这些方法的具体实现可能不一致。 当我们执行foo.name2获取对象值时,就会调用[[Get]]这个 essential internal method。 这个方法在不同的对象中的实现会有不同,如果我们在规范中按关键字进行搜索,我们会得到以下结果, 其中,除了10.1.8是描述ordinary object中的 [[Get]] 算法外,其它小节都是给一些exotic object定义的算法。比如这里就有arguments exotic object、Integer-Indexed exotic object、module namespace exotic object和Proxy exotic object。这里只是列举,暂时我们还不需要考虑这些,如果读者感兴趣,可以自行跳转阅读。 也就是说 [[Get]] 方法与其具体的实现是一对多的关系。 [[Get]] 的定义在规范10.1.8小节中,[[Get]] 方法定义如下, theessential internal methodsused by this specification that are applicable to all objects created or manipulated by ECMAScript code. Every object must have algorithms for all of the essential internal methods. However, all objects do not necessarily use the same algorithms for those methods. Return ?OrdinaryGet(O, P, Receiver).该方法返回的其实是执行OrdinaryGet(O, P, Receiver)的结果。结合我们上边的例子foo.name2来看,foo就是这里的 O,name2是 P。而最后一个参数 Receiver 也是foo对象,稍后我们会讲为什么。 OrdinaryGet方法的步骤如下, Letdescbe ?O.[GetOwnProperty].首先看第 1 步,该方法会通过[[GetOwnProperty]]这个内部方法来获取属性 P 的属性描述符,它返回的结果是一个 Property Descriptor 或者 undefined。 Property Descriptor 属性描述符我们简单介绍一下 Property Descriptor,它直译过来叫做属性描述符,用于描述对象属性的组成和操作。根据属性的不同,它可能会返回两种类型的结果—— DataDescriptor 和 AccessorDescriptor。 //datadescriptor { [[value]]:..., [[Writable]]:..., [[Enumerable]]:..., [[Configurable]]:... } //accessordescriptor { [[Get]]:..., [[Set]]:..., [[Enumerable]]:..., [[Configurable]]:... } 是不是看着有些面熟?没错,当我们调用Object.getOwnPropertyDescriptor()这个 API 时,返回的结果就是在 Property Descriptor 的基础上转换返回的结果。 执行流程回到正题,当步骤 1[[GetOwnProperty]]执行的结果 desc是 Property Descriptor 时,说明此时已经查找到了属性,就会跳过第 2 步,进入步骤 3~7。 这几步做的事情就是解析 desc 返回结果,根据它的类型执行不同的步骤, 如果 desc 类型是 DataDescriptor,则执行步骤 3 直接返回 [[Value]]。如果 desc 类型是 AccessorDescriptor,则执行步骤 4~7 返回 [[Get]] 函数执行结果。当[[GetOwnProperty]]返回的结果 desc 为 undefined 时,说明当前对象上没有定义该属性,此时就会进入步骤 2 。 第二步首先通过[[GetPrototypeOf]]来获取当前对象的原型 parent,获取到之后就在 parent 这个对象执行[[Get]]方法,即 OrdinaryGet。通过这种递归的形式, JavaScript 就会沿着原型链一级级向上查找有没有该属性。如果执行到某次时 parent 为null,则说明已经查找到了原型链的尽头,此时直接返回 undefined,表示未找到该属性。 解析下来整个步骤也比较清晰了,流程如下图所示, 截屏2022-11-03 08.24.49.png所以,规范中对于查找对象属性的流程和我们通常理解的流程基本一致,即沿着原型链一直向上查找,直到找到并返回该属性,或者最终没有找到,返回 undefined。 再深入一点ok,到这里我们已经深入地理解了对象查找属性时所做的事情,但是还有一些事情没有搞清楚。文章开头我们说到,当在对象上查找属性时,会调用 essential internal method,但这是在什么时候调用的呢?规范中有定义吗? 另外,我们刚才提到在调用 OrdinaryGet 方法时,还传入了 Receiver 这个对象,我们现在也没搞清楚它从哪儿来。 接下来,我们再深入一点,从根本上理清这些事情。 语法定义我们现在想要探索清楚的,是当我们通过foo.name2获取值时发生在背后的整个过程。这样我们就不得不提到语法定义了。规范中有一个语法叫做MemberExpression,它长这样, 这种形式初次见到会比较陌生,这是因为规范对于语法的定义采用了context-free grammers这种形式,相关内容我们之后再说。这里我们忽略一些关键字(如 Yield、Await),在这些定义中有一条是MemberExpression.IdentifierName,对应的就是我们上面例子中的foo.name2。 当我们执行 MemberExpression: MemberExpression.IdentifierName 这条运行时语义(Runtime semantics) ,步骤如下, 步骤的重点在于第 4 步的 EvaluatePropertyAccessWithIdentifierKey 方法,规范定义如下, 可以看到这个方法最后会返回一个 Reference Record,形式如下, { [[Base]]:baseValue, [[ReferencedName]]:propertyNameString, [[Strict]]:strict, [[ThisValue]]:empty, } Reference Record 与 GetValueReference Record翻译过来叫做引用记录。注意这里的“引用”指的可不是我们通常意义上的引用对象,不属于 ECMAScript Language Types,而是 ECMAScript Specification Types 中的一种(如果忘了,点击这里)。也就是说 Reference Record 只存在于规范中,是为了更方便的描述规范而产生的。 关于 Reference Record 的内容,我在很早之前的一篇文章中也有提及过,这里就不再赘述了。我们现在只需要知道,Reference Record 定义的是一种中间状态,用于解释 JavaScript 背后的各种行为特性,比如进行deletetypeof等操作背后发生的事情,以及this的指向等等。Reference Record 涉及的范围十分广泛,比如一个表达式的执行结果之前就可能会经历从 Reference Record 取值的过程。 规范中还定义了一个叫做GetValue(V) 的抽象方法,专门用于从 Reference Record 中返回真正的结果。 对于foo.name2这个例子来说,Reference Record 内容如下, { [[Base]]:foo, [[ReferencedName]]:name2, [[Strict]]:strict, [[ThisValue]]:empty, } 当我们执行 GetValue 时,因为 Record 中的 [[Base]] 是一个对象 foo,所以它一个 Property Reference,会进入步骤 3。在步骤 3 中,正常情况下会执行到 3.c. ,即baseObj.[[Get]](V.[[ReferencedName]], GetThisValue(V))。 到这里,我们就已经非常清楚了,[[Get]] 方法是在 GetValue 这个方法中被调用的,它接收的第 3 个参数 Receiver 就是 GetThisValue(V) 的结果。 GetThisValue(V)定义如下, 因为foo.name2拿到的 Reference Record 中的 [[ThisValue]] 为 empty,所以IsSuperReference(V)返回的结果为空。最终 GetThisValue 的执行结果就是 V.[[Base]],即foo对象本身。 那么 Receiver 这个参数有什么用呢?我们来看另一个例子, constfoo={name:'grace'} constbar={name:'walk',getgetName(){returnthis.name;} Object.setPrototypeOf(foo,bar); console.log(foo.getName);//grace 现在你可以解释为什么打印结果是grace而不是walk了吗? 这是因为我们最开始传入的 Receiver 对象会在原型链向上查找的过程中不断被传递,所以this.name这里的 this 指向的始终是foo这个对象。这个时候再看一遍OrdinaryGet的执行过程,你就会豁然开朗。 至此,完整的流程如下图所示, 截屏2022-11-03 08.27.17.png别走,还剩一些疑问...我们在解释foo.name2语法定义的执行步骤时只关注了步骤 4, 那么步骤 1、2 呢?可以看到步骤 1 的结果也是一个 Reference Record,它是从哪里来的,为什么在这里会产生一个 Reference Record?另外,Evaluation 又是什么? Runtime Semantics: Evaluation首先看步骤 1,规范把 MemberExpression 执行的结果赋值给 baseReference。这里的MemberExpression指代的是foo对象。foo 对象的执行过程被定义为Evaluation,在规范中定义如下, IdentifierReference : Identifier Return ?ResolveBinding(StringValue of Identifier).执行过程中返回的是ResolveBinding方法的调用结果。ResolveBinding 我们这里就不展开讲了,它的作用是沿着词法作用域一层层向外查找变量定义,并返回一个 Reference Record 作记录。 所以,步骤1、2 的作用就是在作用域中找到foo对象的定义。这部分还涉及到了规范中关于作用域的部分,我们在后面的章节再讨论。 写在最后在这一章节中,我们以原型链为展开,分析了对象属性查找时的具体流程。在这个过程中,我们认识到了规范中的很多定义,比如 essential internal methods、Property Descriptor 和 Reference Record 等等。这些知识不仅仅与原型链有关,同时与 JavaScript 中的其他内容有着千丝万缕的关系,在后面的章节中,我们不时地还会遇到。 参考资料ECMAScript 规范 Understanding the ECMAScript spec, part 2 最后,欢迎大家一键三连,有大家的支持才有更新的动力嘛~ 阅读原文

上一篇:2023-01-27_计算机视觉和多模态到底需要学什么知识?​AI部署与算法自动驾驶深度学习汇总! 下一篇:2019-04-23_如何让计算机工作环境更便捷?几行简单的命令即可

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价