如何阅读 ECMAScript 规范
原文链接:
https://timothygu.me/es-howto/
问题跟踪:
原作者:
Timothy Gu timothygu99@gmail.com
本作品采用Creative Commons Attribution-ShareAlike 4.0 International License进行许可,该协议可在 https://creativecommons.org/licenses/by-sa/4.0/ 网站上查阅。本作品的部分内容可能来自另一规范文档。如果是这样,则这些部分受该规范文档许可协议的约束。
说明:本文是对原文机翻
摘要
ECMAScript 语言规范(又称 JavaScript 规范或 ECMA-262)是学习 JavaScript 工作原理的重要资源。不过,它篇幅巨大,一开始可能会让人感到困惑和畏惧。本文档旨在帮助读者更轻松地开始阅读 JavaScript 语言的最佳参考资料。
1.前言
您已经决定每天读一点 ECMAScript 规范对您的健康有益。也许这是新年愿望,也许只是医生的处方。无论如何,欢迎加入我们的行列!
注:在本文档中,我仅使用 "ECMAScript "来指代规范本身,而在其他地方则使用 "JavaScript"。不过,这两个术语指的是同一件事。(ECMAScript和JavaScript之间有一些历史上的区别,但讨论这个问题不在本文的讨论范围之内,你可以很容易地在谷歌上找到这些区别)。
1.1.为什么要阅读 ECMAScript 规范
无论是在浏览器 [WHATISMYBROWSER]、服务器上的 Node.js [NODEJS],还是在物联网设备 [JOHNNY-FIVE],ECMAScript 规范都是所有 JavaScript 实现行为的权威来源。所有 JavaScript 引擎的开发人员都依赖于规范,以确保他们闪亮的新功能能像其他 JavaScript 引擎一样按预期运行。
但我认为,该规范的实用性并不局限于那些被称为 "JavaScript 引擎开发者 "的神话人物。事实上,我认为它对你--普通的 JavaScript 程序员--非常有用,只是你还没有意识到而已。
假设有一天,你在工作中发现了以下奇特的并列关系
> Array.prototype.push(42)
1
> Array.prototype
[ 42 ]
> Array.isArray(Array.prototype)
true
> Set.prototype.add(42)
TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
at Set.add (<anonymous>)
> Set.prototype
Set {}并非常困惑为什么一个方法在其原型上有效,而另一个方法却在其原型上无效。不幸的是,谷歌总是在你最需要它的时候失效,而永远有用的 Stack Overflow 也是如此。
阅读规范会有所帮助。
或者,你可能会想知道臭名昭著的松散相等运算符 ( == ) 究竟是如何起作用的(这里的 "作用 "一词是松散的 [WAT])。作为一个勤奋好学的软件工程师,你在 MDN 上查找它,却发现它的解释段落对你的眼睛伤害比帮助更大[MDN]。
阅读规范会有所帮助。
另一方面,我不建议刚接触 JavaScript 的开发人员阅读 ECMAScript 规范。如果你是 JavaScript 的新手,那就先玩玩网络!构建一些网络应用程序!或者一些基于 JavaScript 的保姆式摄像头!或者其他任何东西!等你经历了足够多的 JavaScript 缺陷,或者变得足够富有,不必再为 JavaScript 操心时,再考虑回到本文。
好了,现在你知道规范是非常有用的工具,可以帮助你理解一种语言或平台的复杂性。但 ECMAScript 规范的具体内容是什么呢?
1.2.哪些属于 ECMAScript 规范,哪些不属于
对于这个问题,教科书上的答案是 "只有语言特性才会被纳入 ECMAScript 规范"。但这无济于事,因为这就好比说 "JavaScript 的特性就是 JavaScript"。我不喜欢同义反复 [XKCD-703]。
相反,我要做的是列出 JavaScript 应用程序中常见的一些东西,并告诉你它们是否都是一种语言特性。
[1] ECMAScript 规范规定了此类声明的语法及其含义,但未说明如何加载模块。
[2] 浏览器和 Node.js 中都有这些内容,但都是非标准的。对于 Node.js,它们由其文档记录/指定。对于浏览器,
console由控制台标准 [CONSOLE] 指定,其余部分由 HTML 标准 [HTML] 指定。[3] 这些都是 Node.js 专属的全局项,由其文档记录/指定。* 请注意,与
global不同,globalThis是 ECMAScript 的一部分,也在浏览器中实现。[4] 这些是 Node.js 独有的模块范围内的 "globals",由其文档记录/指定。
[5] 这些都是浏览器专用的东西。
1.3.在进一步讨论之前,ECMAScript 规范在哪里?
当你在谷歌上搜索 "ECMAScript 规范 "时,你会看到很多结果,都声称自己是合法的规范。你应该读哪一个?
简而言之,您更可能需要的是发布在 tc39.es/ecma262/ 上的规范 [ECMA-262]。
长版本:
ECMAScript语言规范是由一群来自不同背景的人开发的,他们被称为Ecma国际技术委员会39(或更熟悉的TC39 [TC39])。TC39 在 tc39.es [ECMA-262].网站上维护最新的 ECMAScript 语言规范。
使问题复杂化的是,TC39 每年都会选择一个时间点对规范进行快照,使其成为当年的 ECMAScript 语言标准,并附上版本号。例如,ECMAScript® 2019语言规范(ECMA-262,10 th 版)[ECMA-262-2019](俗称ES10或ES2019)就是2019年6月在tc39.es [ECMA-262]上看到的规范,它被放入甲醛中,经过适当的收缩包装,并以PDF格式永久存档。
因此,除非您希望自己的网络应用程序只能在 2019 年 6 月以后的浏览器上运行,而这些浏览器已被放入甲醛中,经过适当的收缩包装,并以 PDF 格式永久存档,否则您应该始终关注 tc39.es [ECMA-262]上的最新规范。但如果您希望(或必须)支持旧版本的浏览器或 Node.js,那么参考旧版本的规范可能会有所帮助。
注:ISO/IEC 还将 ECMAScript 语言标准重新发布为 ISO/IEC 22275 [ISO-22275-2018]。不过不用担心,因为该标准基本上是 [ECMA-262] 的超链接。
1.4.浏览规范
ECMAScript 规范涉及大量内容。尽管其作者已尽力将其分成合乎逻辑的几大块,但它仍然是一个庞大的文本。
就我个人而言,我喜欢将规范分为五个部分:
约定和基础知识("什么是 Number?当规范说'抛出 TypeError 异常'时是什么意思?)
语言的语法生成("如何编写
for-in循环?)语言的静态语义("在
var语句中如何确定变量名?)语言的运行时语义("如何执行
for-in循环?)应用程序接口("
String.prototype.substring()做什么?)
但规范并不是这样组织的。相反,它将第一点放在了 §5 Notational Conventions 到 §9 Ordinary and Exotic Objects Behaviours 中,接下来的三点以交错的形式放在了§10 ECMAScript Language: Source Code 到 §15 ECMAScript Language: Scripts and Modules,如
§13.6.1-6 静态语义
§13.6.7 运行时语义
§13.7 迭代语句语法制作
§13.7.1 共享静态和运行时语义
§13.7.2
do-while声明
§13.7.2.1-5 静态语义
§13.7.2.6 运行时语义
§13.7.3
while语句....
而应用程序接口则通过§18 全局对象到§26 反射等条款来传播
在这一点上,我想指出的是,绝对没有人会从上到下阅读规范。相反,只需查看与您想要查找的内容相对应的部分,并在该部分中查看您需要的内容。试着确定您的具体问题与五大部分中的哪一部分相关;如果您难以确定是哪一部分,可以问自己 "这个(您要确认的内容)是在什么时间评估的?别担心,只有多加练习,浏览规范才会变得更容易。
2.运行时语义
语言和应用程序接口的运行时语义是规范中最重要的部分,通常也是人们疑问最多的部分。
总的来说,阅读规范中的这些章节非常简单。不过,规范中使用了很多对刚入门的人来说非常讨厌的速记(至少对我来说是这样)。我将尝试解释其中的一些约定,然后将它们应用到通常的工作流程中,弄清几件事是如何工作的。
ECMAScript 中的大多数运行时语义都是由一系列算法步骤指定的,这与伪代码并无二致,但形式要精确得多。
EXAMPLE 1
A sample set of algorithm steps are:
Let a be
Let b be a+a.
If b is 2, then
Hooray! Arithmetics isn’t broken.
Else
Boo!
进一步阅读§5.2 算法约定
2.2.抽象运算
有时你会在规范中看到一个类似函数的东西被调用。 Boolean() 函数的第一步是:
# When Boolean is called with argument value, the following steps are taken:
# 当调用带有参数值的 Boolean 时,将采取以下步骤:
1. Let b be ! ToBoolean(value).
让 b 成为 !ToBoolean(value).
2. ...ToBoolean "函数被称为抽象操作:之所以说它抽象,是因为它实际上并没有作为一个函数暴露给 JavaScript 代码。它只是规范编写者发明的一种符号,目的是让他们不再重复编写相同的内容。
注意:现在不用担心 ToBoolean 前面的 !我们稍后将在 § 2.4 完成记录;? 和 ! 中讨论。
进一步阅读§5.2.1 抽象操作
2.3.什么是[[This]]
有时,你可能会看到[[Notation]]的用法,比如 "让 proto 成为 obj.[[Prototype]]"。从技术上讲,这个符号可以有多种不同的含义,具体取决于它出现的上下文,但如果你能理解这个符号指的是某些无法通过 JavaScript 代码观察到的内部属性,那么你就会受益匪浅。
确切地说,它可以有三种不同的含义,我将用规范中的例子加以说明。不过,您可以暂时跳过这些例子。
2.3.1.记录的字段
ECMAScript 规范使用 Record 这个术语来指具有一组固定键的键值映射,这有点类似于 C 语言中的结构。Record 中的每个键值对称为一个字段。由于 Records 只能出现在规范中,而不能出现在实际的 JavaScript 代码中,因此使用[[Notation]来指代 Record 的 fields 是合理的。
EXAMPLE 2
值得注意的是,属性描述符也被建模为具有 [[Value]] 、[[Writable]]、[[Get]]、[[Set]]、[[Enumerable]] 和 [[Configurable]] 字段的记录。IsDataDescriptor 抽象操作广泛使用了这种符号:
当使用属性描述符 Desc 调用抽象操作 IsDataDescriptor 时,将采取以下步骤:
1. 如果 Desc 未定义,则返回 false。
2. 如果 Desc.[[值]]和 Desc.[[可写]]都不存在,则返回 false。
3. Return
记录的另一个具体例子见下一节§ 2.4 Completion Records; ? and !。
2.3.2.JavaScript 对象的内部槽
JavaScript 对象可能有所谓的internal slots,规范使用这些内部槽来保存数据。与Record fields一样,这些internal slots也无法通过 JavaScript 观察到,但其中一些可能会通过特定的实现工具(如 Google Chrome 浏览器的 DevTools)暴露出来。因此,使用[[Notation]] 来描述internal slots也是合理的。
internal slots的具体内容将在第 2.5 节 JavaScript 对象中介绍。现在,不用太在意它们的用途,但请注意下面的示例。
EXAMPLE 3
大多数 JavaScript 对象都有一个内部槽[[原型]],指的是它们所继承的对象。这个内部槽的值通常就是 Object.getPrototypeOf() 返回的值。在 OrdinaryGetPrototypeOf 抽象操作中,将访问该内部槽的值:
当使用对象 O 调用抽象操作 OrdinaryGetPrototypeOf 时,将采取以下步骤:
Return O.[[Prototype]].
注意:Objects 和 Record 字段的内部槽在外观上是相同的,但可以通过观察此符号的先例(点之前的部分)来区分它们是 Object 还是 Record。从周围的上下文来看,这一点通常相当明显。
2.3.3.JavaScript 对象的内部方法
JavaScript 对象还可能有所谓的内部方法。与内部槽一样,这些内部方法无法通过 JavaScript 直接观察到。因此,使用[[Notation]]来描述内部方法也是合理的。
内部方法的具体内容将在第 2.5 节 JavaScript 对象中介绍。现在,不用太在意它们的用途,但请注意下面的示例。
EXAMPLE 4
所有 JavaScript 函数都有一个运行该函数的内部方法[[Call]]。调用抽象操作有以下步骤
3. Return ? F.[[Call]](V, argumentsList).
其中 F 是一个 JavaScript 函数对象。在这种情况下,F 的[[Call]]内部方法本身会连同参数 V 和 argumentsList 一起被调用。
注:[[Notation]]的第三种意义可以通过看起来像函数调用来与其他意义区分开来。
2.4.完成记录; ? 和 !
ECMAScript 规范中的每个运行时语义都会显式或隐式地返回一个报告其结果的 "完成记录"(Completion Record)。该完成记录是一个有三个可能字段的记录:
a [[Type]] (
normal,return,throw,break, 或continue)如果[[Type]]是
normal、return或throw,那么它也可以有一个[[值]]("返回/抛出的内容")如果[[Type]]是
break或continue,则可以选择携带一个称为[[Target]]的标签。 由于这种运行时语义,脚本执行会中断/继续执行
注:两个括号用于表示记录的字段。请参阅§ 2.3.1 记录的字段,了解记录及其相关符号。
[[Type]]为 normal 的完成记录称为正常完成。正常完成以外的每条完成记录也称为突然完成。
大多数情况下,你只需要处理[[Type]]为 throw 的突然补全。其他三种突然补全类型只在查看特定语法元素如何求值时有用。事实上,在内置函数的定义中,你永远不会看到任何其他类型,因为 break / continue / return 不能跨越函数边界。
进一步阅读§6.2.3 完成记录规范类型
因为有了完成记录的定义,JavaScript 中诸如在 try - catch 块之前冒泡出错之类的小技巧在规范中就不存在了。事实上,错误(或者更确切地说是突然完成)会被明确处理。
在没有任何速记符号的情况下,对抽象操作的普通调用(可能返回计算结果,也可能抛出错误)的规范文本如下所示:
EXAMPLE 5
调用抽象操作的几个步骤,这些抽象操作可能会抛出,但没有任何速记符号:
1.让 resultCompletionRecord 成为 AbstractOp()。
注意:resultCompletionRecord 是完成记录。
2.如果 resultCompletionRecord 是突然完成,则返回 resultCompletionRecord。
注:在此,如果是突然完成,则直接返回 resultCompletionRecord。换句话说,在 AbstractOp 中抛出的错误会被转发,剩余的步骤会被中止。
3.让 result 成为 resultCompletionRecord.[[Value]]。
注意:在确保正常完成后,我们现在可以拆开 "完成记录",以获得所需的实际计算结果。
4.结果就是我们需要的结果。我们现在可以用它做更多的事情
这可能会让你依稀想起 C 语言中的手动错误处理:
int result = abstractOp(); // Step 1 if (result < 0) // Step 2 return result; // Step 2 (continued) // Step 3 is unneeded // func() succeeded; carrying on... // Step 4
但是,为了减少这些繁琐的步骤,ECMAScript 规范的编辑们添加了一些速记符号。自 ES2016 起,相同的规范文本可以用以下两种等效的方式编写:
EXAMPLE 6
调用抽象操作的几个步骤,可能会抛出 ReturnIfAbrupt:
1.让 result 成为 AbstractOp()。
注意:这里与上一个示例中的步骤 1 一样,结果是一个完成记录。
2.ReturnIfAbrupt(result).
注:ReturnIfAbrupt 通过转发来处理任何可能的突然完成,并自动将结果解包为其[[值]]。
3.结果就是我们需要的结果。我们现在可以用它做更多的事情。
或者更简洁地用一个特殊的问号(?)
EXAMPLE 7
调用抽象操作的几个步骤,可能会出现问号(?):
1.让结果成为 ?AbstractOp().
注意:在这个符号中,我们根本不处理完成记录。我们使用"...... "速记来处理一切,之后就可以立即使用结果了。
2.结果就是我们需要的结果。我们现在可以用它做更多的事情。
有时,如果我们知道对 AbstractOp 的特定调用永远不会返回突然的完成,就可以向读者传达更多有关规范意图的信息。在这种情况下,我们会使用感叹号(!):
...未完待续
参考资料
5.1开卷有益
[CONSOLE]
Dominic Farolino; Terin Stock; Robert Kowalski. Console Standard. Living Standard. URL: https://console.spec.whatwg.org/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMA-262]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/
[ECMA-262-2019]
ECMAScript 2019 Language Specification. URL: https://ecma-international.org/ecma-262/10.0/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[ISO-22275-2018]
ISO/IEC 22275:2018 - Information technology — Programming languages, their environments, and system software interfaces — ECMAScript® Specification Suite. URL: https://www.iso.org/standard/73002.html
[JOHNNY-FIVE]
Johnny-Five: The JavaScript Robotics & IoT Platform. URL: http://johnny-five.io/
[MDN]
Mozilla Developer Network. URL: https://developer.mozilla.org/en-US/
[NODEJS]
Node.js. URL: https://nodejs.org/
[TC39]
TC39 - ECMAScript. URL: https://www.ecma-international.org/memento/tc39.htm
[WAT]
Gary Bernhardt. Wat. URL: https://www.destroyallsoftware.com/talks/wat
[WHATISMYBROWSER]
What browser am I using?. URL: https://www.whatsmybrowser.org/
[XKCD-703]
Randall Munroe. xkcd: Honor Societies. URL: https://www.xkcd.com/703/
[YDKJS]
Kyle Simpson. You Don't Know JS (book series). URL: https://github.com/getify/You-Dont-Know-JS