文章

如何阅读 ECMAScript 规范

原文链接:

https://timothygu.me/es-howto/

问题跟踪:

GitHub

Inline In Spec

原作者:

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 应用程序中常见的一些东西,并告诉你它们是否都是一种语言特性。

语法元素的语法(即一个有效的 for ... in 循环是什么样的)

句法元素的语义(即 typeof null{ a: b } 返回的内容)

import a from 'a';

[1]

Object, Array, Function, Number, Math, RegExp, Proxy, Map, Promise, ArrayBuffer, Uint8Array, globalThis, ...

console, setTimeout(), setInterval(), clearTimeout(), clearInterval()

[2]

Buffer, process, global*

[3]

module, exports, require(), __dirname, __filename

[4]

window, alert(), confirm(), the DOM (document, HTMLElement, addEventListener(), Worker, ...)
window , alert() , confirm() , DOM ( document , HTMLElement , addEventListener() , Worker , ...)

[5]

[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 if 语句语法制作

    • §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:

  1. Let a be

  2. Let b be a+a.

  3. If b is 2, then

    1. Hooray! Arithmetics isn’t broken.

  4. Else

    1. 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]来指代 Recordfields 是合理的。

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 时,将采取以下步骤:

  1. Return O.[[Prototype]].

注意:Objects 和 Record 字段的内部槽在外观上是相同的,但可以通过观察此符号的先例(点之前的部分)来区分它们是 Object 还是 Record。从周围的上下文来看,这一点通常相当明显。

2.3.3.JavaScript 对象的内部方法

JavaScript 对象还可能有所谓的内部方法。与内部槽一样,这些内部方法无法通过 JavaScript 直接观察到。因此,使用[[Notation]]来描述内部方法也是合理的。

内部方法的具体内容将在第 2.5 节 JavaScript 对象中介绍。现在,不用太在意它们的用途,但请注意下面的示例。

EXAMPLE 4


所有 JavaScript 函数都有一个运行该函数的内部方法[[Call]]。调用抽象操作有以下步骤

  1. 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]]是 normalreturnthrow ,那么它也可以有一个[[值]]("返回/抛出的内容")

  • 如果[[Type]]是 breakcontinue ,则可以选择携带一个称为[[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

License:  CC BY 4.0