如何阅读 ECMAScript 规范

动态文档,

当前版本:
https://timothygu.me/es-howto/
问题追踪:
GitHub
规范内联
作者:
Timothy Gu

摘要

ECMAScript 语言规范(也称为 JavaScript 规范,或 ECMA-262)是学习 JavaScript 工作原理细节的绝佳资源。然而,它是一份庞大的文本,初学者可能会觉得困惑和望而生畏。本文档旨在帮助你更容易入门,阅读最权威的 JavaScript 语言参考资料。

1. 前言

你决定每天读一点 ECMAScript 规范,对你的健康有益。也许这是你的新年愿望,或者是医生的建议。不管是什么,欢迎加入!

注意: 在本文档中,我只用“ECMAScript”表示规范本身,在其他地方用“JavaScript”。不过,这两个词指的是同一个东西。(ECMAScript 和 JavaScript 之间有一些历史上的区别,但讨论这些不在本文范围内,你可以自行搜索这个区别。)

1.1. 为什么要阅读 ECMAScript 规范

ECMAScript 规范是所有 JavaScript 实现行为的权威来源,无论是在你的浏览器[WHATISMYBROWSER],在 Node.js 服务器端[NODEJS],还是在你的物联网设备上[JOHNNY-FIVE]。所有 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 {}

你很困惑为什么一个方法在它的 prototype 上能用,而另一个方法却在它的 prototype 上不能用。Google 在你最需要时总是帮不了你Stack Overflow 也不灵

阅读规范可以帮你。

或者,你可能会疑惑臭名昭著的宽松等号运算符==)到底是怎么工作的(这里“function”也用得很宽松[WAT])。你很勤奋,去 MDN 查找,却发现相关的解释段落让你越看越晕[MDN]

阅读规范可以帮你。

另一方面,我不建议刚接触 JavaScript 的开发者直接阅读 ECMAScript 规范。如果你是新手,先玩玩网页!做点 web 应用!或者做点基于 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(), DOM (document, HTMLElement, addEventListener(), Worker, ...) [5]
[1] ECMAScript 规范规定了这种声明的语法和它本应表达的意义,但并未规定如何加载模块。

[2] 这些东西在浏览器和 Node.js 都能用,但它们不是标准的一部分。Node.js 的相关内容在官方文档中有说明。浏览器端,console 由 Console Standard 定义[CONSOLE],其余由 HTML 标准定义[HTML]

[3] 这些都是 Node.js 独有的全局变量,相关内容在官方文档中说明。* 注意和 global 不同,globalThis 是 ECMAScript 的一部分,在浏览器中也有实现。

[4] 这些是 Node.js 独有的模块级“全局”,相关内容在官方文档中说明。

[5] 这些都是浏览器独有的东西。

1.3. 在继续之前,ECMAScript 规范在哪里?

当你Google “ECMAScript 规范”时,会看到一大堆结果,都号称是官方规范。到底该看哪个??

简而言之,最可能你需要的规范就是 tc39.es/ecma262/ 发布的那个 [ECMA-262]

详细版:

ECMAScript 语言规范由一个来自不同背景的团队制定,叫做 Ecma International Technical Committee 39(通常简称 TC39[TC39])。TC39 在 tc39.es 上维护 ECMAScript 语言的最新规范[ECMA-262]

麻烦的是,每年 TC39 都会选定某个时间点,把规范快照下来,变成当年的 ECMAScript 语言标准,并给出版本号。例如,ECMAScript® 2019 Language Specification (ECMA-262, 10) [ECMA-262-2019](俗称 ES10 或 ES2019)就是 2019 年 6 月 tc39.es 上的规范,做成 PDF 存档。

因此,除非你只想让你的 web 应用跑在 2019 年 6 月的浏览器上,并且这些浏览器也被做成 PDF 存档,否则你应该始终查阅 tc39.es 上最新的规范[ECMA-262]。但如果你需要兼容旧版浏览器或旧版 Node.js,可以参考历史规范。

注意: ISO/IEC 也会以 ISO/IEC 22275 [ISO-22275-2018] 重新发布 ECMAScript 标准。但不用担心,这标准其实就是跳转到[ECMA-262]

ECMAScript 规范讲的东西非常多。尽管编写者尽量把它分成逻辑块,但整体还是很庞大。

我个人喜欢把规范分成五部分:

但规范不是这样组织的。它将第一个部分放在§5 记号约定§9 普通和特殊对象行为,接下来的三部分交错在§10 ECMAScript 语言:源代码§15 ECMAScript 语言:脚本和模块之间,比如:

  • §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 语句

      • ...

而 API 分布在§18 全局对象§26 反射之间。

这里有个重点:几乎没人会从头到尾读规范。你只需查找你关心的部分,在对应章节只看需要的内容。试着判断你的问题属于以上五大类中的哪一类;如果判断不了,可以问自己“这东西(你想确认的内容)在哪个阶段被评估?”这样或许有帮助。别担心,规范越读越顺手。

2. 运行时语义

语言和 API 的运行时语义是规范中最大的一部分,也往往是大家最关心的内容。

整体来看,规范里的这些章节其实挺好读。不过规范用到不少简写方式,对刚入门的人来说可能有点难(至少对我来说是)。我会解释一些常见的约定,并用实际流程说明这些东西怎么用。

2.1. 算法步骤

ECMAScript 里的大多数运行时语义都是用一系列算法步骤来描述的,类似伪代码,但更加精确。

一组算法步骤示例:

  1. a1

  2. ba+a

  3. 如果 b 等于 2,则

    1. 万岁!算术没坏。

  4. 否则

    1. 呜呜!

延伸阅读:§5.2 算法约定

2.2. 抽象操作

你有时会在规范里看到类似函数调用的结构。比如 Boolean() 函数的第一步是:

当用参数 value 调用 Boolean 时,执行以下步骤:

  1. b 为 ! ToBoolean(value)。

  2. ...

这个 “ToBoolean” 就是所谓的抽象操作:叫“抽象”是因为它并不作为函数暴露给 JavaScript 代码使用。它只是规范作者发明的一个记法,方便不重复描述同样的逻辑。

注意: 暂时不用管 ToBoolean 前面的 !,我们后面在 § 2.4 完成记录;? 和 ! 再讲。

延伸阅读:§5.2.1 抽象操作

2.3. 什么是 [[This]]

有时你会看到 [[记号]] 被用在类似“令 protoobj.[[Prototype]]。”这样的语句中。这个记号在不同语境下可以表示几种不同的东西,不过只要记住它指的是某种 JavaScript 代码无法直接访问的内部属性,你就能用很久了。

严格来说,它可能表示三种东西,下面用规范里的例子说明。当然你暂时可以跳过这些内容。

2.3.1. Record 的字段

ECMAScript 规范使用 Record 指代一个键值固定的映射——有点像 C 语言的结构体。Record 的每个键值对都叫一个 字段。由于 Record 只出现在规范里,实际 JavaScript 代码里没有,因此用 [[记号]] 来引用 字段很合理。

值得注意的是,属性描述符也是用 Record 建模的,拥有 字段 [[Value]]、[[Writable]]、[[Get]]、[[Set]]、[[Enumerable]] 和 [[Configurable]]。IsDataDescriptor 抽象操作大量使用了这种记号:

当用 属性描述符 Desc 调用 IsDataDescriptor 抽象操作时,执行如下步骤:

  1. 如果 Descundefined,返回 false

  2. 如果 Desc 的 [[Value]] 和 [[Writable]] 都不存在,返回 false

  3. 返回 true

在下一节§ 2.4 完成记录;? 和 !还有另一个 Record 的例子。

延伸阅读:§6.2.1 List 和 Record 规范类型

2.3.2. JavaScript 对象的内部槽

JavaScript 对象可能有一些内部槽,规范用来存放数据。和 Record 字段一样,这些内部槽也无法用 JavaScript 直接访问,但有些可以通过实现相关工具(比如 Chrome DevTools)看到。因此用 [[记号]]来描述内部槽也很合适。

内部槽的细节会在§ 2.5 JavaScript 对象中详细介绍。现在不用纠结它们有什么用,只需看下面这个例子:

大多数 JavaScript 对象都有一个内部槽 [[Prototype]],指向它继承的对象。这个内部槽的值通常就是 Object.getPrototypeOf() 返回的内容。在 OrdinaryGetPrototypeOf 抽象操作里,这个内部槽是这样访问的:

当用对象 O 调用 OrdinaryGetPrototypeOf 抽象操作时,执行以下步骤:

  1. 返回 O.[[Prototype]]。

注意: 对象的内部槽Record 字段在外观上是一样的,但可以通过点号前面的内容来区分(是对象还是Record)。通常根据上下文很容易分辨。

2.3.3. JavaScript 对象的内部方法

JavaScript 对象也可能有所谓的 内部方法。像 内部槽一样,这些 内部方法不能通过 JavaScript 直接观察。因此,用 [[记号]]描述 内部方法也是合理的。

内部方法的具体细节会在 § 2.5 JavaScript 对象介绍。现在不用纠结它们的具体用途,但可以先看看下面的例子。

所有 JavaScript 函数都有一个内部方法 [[Call]],用于运行该函数。Call 抽象操作有如下步骤:

  1. 返回 ? F.[[Call]](V, argumentsList)。

其中 F 是 JavaScript 函数对象。在这里,F 的 [[Call]] 内部方法会用参数 VargumentsList 被调用。

注意: 这种 [[记号]] 的第三种用法可以通过“像函数调用”这一外观与其他用法区分。

2.4. 完成记录;?!

ECMAScript 规范中的每个运行时语义都会显式或隐式返回一个 完成记录,用于报告其结果。这个 完成记录是一个 Record,拥有三种可能的 字段

注意: 两个括号用于表示 Record 字段。见 § 2.3.1 Record 的字段,了解 Record 及相关记号。

完成记录[[Type]]normal 时,被称为 正常完成。其他所有 完成记录,除了 正常完成,也称为 异常完成

大多数时候,你只会遇到 异常完成,其 [[Type]]throw。其他三种异常完成类型主要用于说明某些语法元素如何被求值。实际上,你在内置函数定义里不会见到这些类型,因为 break/continue/return 不会跨函数边界起作用。

延伸阅读:§6.2.3 完成记录规范类型


由于 完成记录的定义,JavaScript 里的错误冒泡(直到 try-catch 块)这种机制在规范中并不存在。实际上,错误(更准确地说是异常完成)都是显式处理的。

如果不用任何简写,一个可能返回计算结果或抛出错误的 抽象操作的规范文本是:

调用可能抛错的抽象操作的几个步骤,不使用任何简写

  1. resultCompletionRecord 为 AbstractOp()。

    注意: resultCompletionRecord 是一个 完成记录

  2. 如果 resultCompletionRecord 是异常完成,返回 resultCompletionRecord

    注意: 这里,resultCompletionRecord 如果是 异常完成,就直接返回。换言之,AbstractOp 里抛出的错误会被直接转发,后续步骤会被中止。

  3. resultresultCompletionRecord.[[Value]]。

    注意: 确认拿到 正常完成后,可以把 完成记录解包,得到我们需要的计算结果。

  4. result 就是我们需要的结果,可以继续操作。

这可能让你想到 C 语言里的手动错误处理:

int result = abstractOp();              // 步骤1
if (result < 0)                         // 步骤2
  return result;                        // 步骤2(续)
                                        // 步骤3不需要
// func() 成功,可以继续...     // 步骤4

但为了减少这些模板化的步骤,ECMAScript 规范编辑者引入了一些简写。从 ES2016 起,可以用如下两种等价方式写同样的规范:

调用可能抛错的抽象操作的几个步骤,使用 ReturnIfAbrupt

  1. result 为 AbstractOp()。

    注意: 这里和上一个例子的第一步一样,result 是一个 完成记录

  2. ReturnIfAbrupt(result)。

    注意: ReturnIfAbrupt 会处理所有可能的 异常完成,自动转发并解包 result[[Value]]

  3. result 就是我们需要的结果,可以继续操作。

或者更简洁地,用一个特殊的 问号 (?)记号:

调用可能抛错的抽象操作的几个步骤,问号 (?) 简写

  1. result 为 ? AbstractOp()。

    注意: 用这种记号就不用管 完成记录? 简写会帮我们全部处理好,result 立刻可用。

  2. result 就是我们需要的结果,可以继续操作。


有时,如果我们确定某次 AbstractOp 调用绝不会返回异常完成,给规范读者传达更多意图,可以用 感叹号 (!)

调用绝不会抛错的抽象操作的几个步骤,感叹号 (!) 简写

  1. result 为 ! AbstractOp()。

    注意: ? 会转发所有可能的错误,! 则断言这次调用绝不会有异常完成,否则规范就有 bug。和 ? 一样,这里也不用管 完成记录result 立刻可用。

  2. result 就是我们需要的结果,可以继续操作。

注意

如果 ! 看起来像有效的 JavaScript 表达式,可能会让人困惑:

  1. b 为 ! ToBoolean(value)。

——摘自 Boolean()

这里,! 只是表示我们确定这次 ToBoolean 调用绝不会抛异常,不是结果取反!

延伸阅读:§5.2.3.4 ReturnIfAbrupt 简写

2.5. JavaScript 对象

在 ECMAScript 中,每个对象都有一组 内部方法,规范其它部分会调用这些方法完成某些任务。所有对象都有的一些 内部方法包括:

(完整列表见 §6.1.7.2 对象内部方法与内部槽)。

基于这个定义,函数对象(或称“函数”)就是额外拥有 [[Call]] 内部方法、可能还有 [[Construct]] 内部方法的对象,因此也叫 可调用对象

规范随后将所有对象分为两类:普通对象特殊对象。大部分遇到的对象是 普通对象,即它们的所有 内部方法都是 §9.1 普通对象内部方法与内部槽规定的默认实现。

不过,ECMAScript 规范还定义了几种 特殊对象,它们可以重写这些内部方法的默认实现。规范对特殊对象的行为有限制,但总体上重写后的内部方法可以做很多花活,并不违反规范。

Array 对象就是一种 特殊对象。比如 length 属性的一些特殊语义,普通对象的机制无法实现。

比如,把 length 属性赋值给一个 Array 对象会移除其属性,但 length 又只是一个普通的数据属性。相比之下,new Map().size 只是 Map.prototype 上定义的 getter,并没有 [].length 的魔法。

> const arr = [0, 1, 2, 3];
> console.log(arr);
[ 0, 1, 2, 3 ]
> arr.length = 1;
> console.log(arr);
[ 0 ]
> console.log(Object.getOwnPropertyDescriptor([], "length"));
{ value: 1,
  writable: true,
  enumerable: false,
  configurable: false }
> console.log(Object.getOwnPropertyDescriptor(new Map(), "size"));
undefined
> console.log(Object.getOwnPropertyDescriptor(Map.prototype, "size"));
{ get: [Function: get size],
  set: undefined,
  enumerable: false,
  configurable: true }

这种行为是通过重写 [[DefineOwnProperty]] 内部方法实现的。详细见 §9.4.2 数组特殊对象

ECMAScript 规范也允许其他规范定义自己的特殊对象。浏览器对跨域 API 访问的限制就是通过这种机制指定的(见 WindowProxy[HTML]。JavaScript 编程者也可以通过 Proxy API 创建自己的特殊对象。


JavaScript 对象还可以定义 内部槽,用于保存某种值。我倾向于把 内部槽看作连 Object.getOwnPropertySymbols() 都拿不到的 Symbol 命名属性。普通对象特殊对象都允许有 内部槽

§ 2.3.2 JavaScript 对象的内部槽里,我提到大多数对象都有一个叫 [[Prototype]] 的 内部槽。(实际上所有 普通对象,甚至一些 特殊对象Array 对象都有。)但我们也知道有一个叫 [[GetPrototypeOf]] 的 内部方法,上面简要介绍过。那么它们有什么区别?

关键在于 大多数:虽然大多数对象都有 [[Prototype]] 内部槽,但所有对象都实现 [[GetPrototypeOf]] 内部方法。需要注意的是,Proxy 对象没有自己的 [[Prototype]],其 [[GetPrototypeOf]] 内部方法会转发到注册的 handler 或目标对象的原型,该原型存储在 Proxy 对象的 [[ProxyTarget]] 内部槽里。

因此,处理对象时,几乎总是应该查阅对应的 内部方法,而不是直接看 内部槽的值。


另一个理解对象、内部方法内部槽之间关系的方法,是用经典面向对象的视角。“对象”就像一个接口,规定了必须实现哪些 内部方法普通对象提供默认实现,特殊对象可能部分或全部重写。另一方面,内部槽就像对象的实例变量,是对象的实现细节。

这些关系都可以用下图 UML 表示(点击放大):

Boxes denoting concepts and connections between them denoting hierarchy

2.6. 示例:String.prototype.substring()

现在我们已经对规范的结构和写法有了很好的理解,让我们来练习一下!

假设我现在有如下问题:

不运行代码,下面的代码片段会返回什么?

String.prototype.substring.call(undefined, 2, 4)

这是个很刁钻的问题。看起来有两种合理的结果:

  1. String.prototype.substring() 可能会先把 undefined 转为字符串 "undefined",然后取第2和第3个字符(即区间 [2, 4)),结果为 "de"

  2. 另一方面,String.prototype.substring() 也有可能直接抛出错误,拒绝 undefined 作为输入。

不幸的是,MDN 也没有说明当 this 不是字符串时函数会有什么表现。

规范来拯救! 在规范页面左上角搜索框输入 substring,我们就会到达 §21.1.3.22 String.prototype.substring ( start, end ),这是该函数的权威定义。

在读算法步骤前,先想想我们已知什么。假定我们知道 str.substring() 一般是返回字符串的一部分。现在不确定的是 thisundefined 时会怎样。所以我们要重点关注涉及 this 的算法步骤。

幸运的是,String.prototype.substring() 的算法第一步就是处理 this

  1. O 为 ? RequireObjectCoercible(this value)。

? 简写说明 RequireObjectCoercible 抽象操作有可能抛异常,否则应该用 !。实际上如果抛错就和我们的第二种猜测吻合!我们跟进 RequireObjectCoercible 的链接看看。

RequireObjectCoercible 抽象操作有点特殊,不像大多数 抽象操作用步骤定义,而是用表:

参数类型 结果
Undefined 抛出 TypeError 异常。
... ...

没关系——在 Undefined(我们传递的 this 类型)那一行,规范明确说 RequireObjectCoercible 应该抛出异常。而且因为函数定义用的是 ?,我们知道异常会冒泡到函数调用者。搞定!

所以答案是:给定代码片段会抛出 TypeError 异常。

规范只规定了抛出的错误类型,没有规定错误消息。也就是说不同实现的错误消息可能不同,甚至可能是本地化的。

比如 Google 的 V8 6.4(Chrome 64)消息是:

TypeError: String.prototype.substring called on null or undefined

而 Mozilla Firefox 57.0 的消息则更简单:

TypeError: can’t convert undefined to object

同时,ChakraCore 1.7.5.0(Microsoft Edge 的 JS 引擎)采用了 V8 风格:

TypeError: String.prototype.substring: 'this' is null or undefined

2.7. 示例:Boolean()String() 会抛异常吗?

写关键代码时,异常处理很重要。所以“某个内建函数到底会不会抛异常?”这个问题常常被考虑。

本例我们尝试回答 Boolean()String() 这两个内建函数是否会抛异常。这里只看直接调用这两个函数,不考虑 new Boolean()new String()(会返回装箱对象——几乎是JS 最不推荐的特性之一,几乎所有风格指南都不建议用[YDKJS])。

查到规范里 Boolean() 的章节,发现算法步骤非常简洁:

当用参数 value 调用 Boolean 时,执行以下步骤:

  1. b 为 ! ToBoolean(value)。

  2. 如果 NewTarget 是 undefined,返回 b

  3. O 为 ? OrdinaryCreateFromConstructor(NewTarget, "%BooleanPrototype%", « [[BooleanData]] »)。

  4. 设置 O.[[BooleanData]] 为 b

  5. 返回 O

但其实没那么简单,第三步涉及 OrdinaryCreateFromConstructor,还有 ? 简写,说明有可能抛异常。我们仔细看看。

第1步把 value(函数参数)转成布尔值。注意没有 ?! 简写,通常没有 Completion Record 简写就等价于 !。所以第1步不会抛异常。

第2步判断 NewTarget 是否 undefinedNewTarget 是规范里 new.target 元属性的等价物,可以让规范区分 new Boolean()(值为 Boolean)和 Boolean()(值为 undefined)。我们这里只看直接调用 Boolean(),所以 NewTarget 一定是 undefined,算法会直接返回 b,不会继续后面的处理。

所以直接调用 Boolean() 只会走到前两步,而这两步都不会抛异常,因此 Boolean() 无论输入是什么都不会抛异常


再来看 String()

当用参数 value 调用 String 时,执行以下步骤:

  1. 如果没有传参数,令 s""

  2. 否则,

    1. 如果 NewTarget 是 undefinedType(value) 是 Symbol,返回 SymbolDescriptiveString(value)。

    2. s 为 ? ToString(value)。

  3. 如果 NewTarget 是 undefined,返回 s

  4. 返回 ? StringCreate(s, ? GetPrototypeFromConstructor(NewTarget, "%StringPrototype%"))。

参照分析 Boolean() 的经验,我们知道 NewTarget 对于我们的情况总是 undefined,所以最后一步不用管。TypeSymbolDescriptiveString 也安全,因为都不会有异常完成。关键是对 ? 前缀的 ToString 抽象操作。我们仔细看。

和前面讲过的 RequireObjectCoercible 类似,ToString(argument) 也是用表定义:

参数类型 结果
Undefined 返回 "undefined"
Null 返回 "null"
Boolean 如果 argumenttrue,返回 "true"

如果 argumentfalse,返回 "false"

Number 返回 NumberToString(argument)。
String 返回 argument
Symbol 抛出 TypeError 异常。
Object

按以下步骤操作:

  1. primValue 为 ? ToPrimitive(argument, hint String)。

  2. 返回 ? ToString(primValue)。

ToStringString() 调用时, value 可以是 Symbol 之外的任何值(因为前一步已经过滤掉 Symbol)。但是 Object 那一行还是有两个 ?。我们可以继续跟进 ToPrimitive 等链接,会发现如果 value 是 Object,规范允许很多抛错的场景:

一些 String() 抛异常的例子
// 规范调用栈:
//   OrdinaryGet 第8步。
//   普通对象 [[Get]]() 第1步。
//   GetV 第3步。
//   GetMethod 第2步。
//   ToPrimitive 第2.d。

String({
  get [Symbol.toPrimitive]() {
    throw new Error("Breaking JavaScript");
  }
});
// 规范调用栈:
//   GetMethod 第4步。
//   ToPrimitive 第2.d步。

String({
  get [Symbol.toPrimitive]() {
    return "Breaking JavaScript";
  }
});
// 规范调用栈:
//   ToPrimitive 第2.e.i步。

String({
  [Symbol.toPrimitive]() {
    throw new Error("Breaking JavaScript");
  }
});
// 规范调用栈:
//   OrdinaryToPrimitive 第5.b.i步。
//   ToPrimitive 第2.g步。

String({
  toString() {
    throw new Error("Breaking JavaScript");
  }
});
// 规范调用栈:
//   OrdinaryToPrimitive 第5.b.i步。
//   ToPrimitive 第2.g步。

String({
  valueOf() {
    throw new Error("Breaking JavaScript");
  }
});
// 规范调用栈:
//   OrdinaryToPrimitive 第6步。
//   ToPrimitive 第2.g步。

String(Object.create(null));

所以对于 String(), 结论是 对于原始值永不抛异常,但对于对象 有可能 抛异常。

2.8. 示例:typeof 运算符

前面我们只分析了 API 函数,这次试试别的内容。

待补充。<https://github.com/TimothyGu/es-howto/issues/2>

术语表

常见抽象操作

ArrayCreate ( length [ , proto ] ) (规范)

创建一个长度为 length 的数组对象,proto 作为其 [[Prototype]] 内部槽的值。如果未指定 proto,则使用 %ArrayPrototype%(当前环境 current realm 中的)。等价于 new Array(length),前提是 Array 构造函数及其所有属性未被篡改,并且未指定 proto%ArrayPrototype%(当前环境)。

Call ( F, V [ , argumentsList ] ) (规范)
Construct ( F [ , argumentsList [ , newTarget ] ] ) (规范)
Get ( O, P ) (规范)
HasProperty ( O, P ) (规范)

调用 FO 上对应的 内部方法,并把其余参数一并传递。等价于 Reflect 对象上的对应方法。

DefinePropertyOrThrow ( O, P, desc ) (规范)
DeletePropertyOrThrow ( O, P ) (规范)

O 上调用对应的 内部方法(分别为 [[DefineOwnProperty]] 和 [[Delete]]),并传递剩余参数。如果操作失败且 内部方法返回 false,则抛出异常。

GetV ( V, P ) (规范)

返回 Get(V, P),必要时会先用 ToObjectV 转换为对象。等价于 V[P]

HasOwnProperty ( O, P ) (规范)

返回 O 是否拥有名为 P 的自有属性,通过调用 O.[[GetOwnProperty]](P)。等价于 Object.prototype.hasOwnProperty.call(O, P)

Invoke ( V, P [ , argumentsList ] ) (规范)

V 上调用名为 P 的方法,并传递 argumentsList。等价于 V[P](...argumentsList)Call 不同,这里的 P 是属性键。

IsArray ( argument ) (规范)

返回 argument 是否为 Array 特殊对象,或者如果 argumentProxy 特殊对象,则判断其最内层的 [[ProxyTarget]] 内部槽是否为 Array 特殊对象。等价于 Array.isArray(argument)

IsCallable ( argument ) (规范)

返回 argument 是否为 可调用对象,也就是 函数对象。等价于 typeof argument === 'function' (但 document.all 例外,它是一个有特殊行为的特殊对象;见 §B.3.7 [[IsHTMLDDA]] 内部槽)。

IsConstructor ( argument ) (规范)

返回 argument 是否为带有 [[Construct]] 内部方法函数对象

ReturnIfAbrupt ( argument ) (规范)

检查 argument 是否为 异常完成(比如抛出的异常),如果是则返回该 异常完成(让异常继续冒泡)。否则如果 argument正常完成,则解包该 完成记录,并把 argument 赋值为 argument.[[Value]]

参见:§ 2.4 完成记录;? 和 !

StringCreate ( value, prototype ) (规范)

返回一个对应于字符串 value 的包装 String 对象,其 [[Prototype]] 内部槽为 prototype。等价于 new String(value),前提是 prototype%StringPrototype%(当前环境 current realm)。

ToBoolean ( argument ) (规范)

返回 argument 强制转换为布尔值。等价于 !!argument

ToInteger ( argument ) (规范)

返回 ToNumber(argument),然后截断(即四舍五入到0)为整数。等价于 Math.trunc(argument)

ToInt8 ( argument ) (规范)
ToUint8 ( argument ) (规范)
ToInt16 ( argument ) (规范)
ToUint16 ( argument ) (规范)
ToInt32 ( argument ) (规范)
ToUint32 ( argument ) (规范)

返回 argument 按指定位数和有符号性转换为整数,方式为截断。

ToUint8Clamp ( argument ) (规范)

返回 argument 通过四舍五入和截断转换为 [0, 255] 范围内的整数。

ToNumber ( argument ) (规范)

返回 argument 强制转换为数字。等价于 +argument

ToObject ( argument ) (规范)

返回 argument 强制转换为对象,必要时使用包装原始对象。等价于 Object(argument),但 argumentundefinednull 时例外。

ToPrimitive ( input [ , PreferredType ] ) (规范)

返回 input 强制转换为原始值(即非对象),可以通过 PreferredType 类型提示指定类型。该 抽象操作的具体语义取决于 PreferredType

ToString ( argument ) (规范)

返回 argument 强制转换为字符串。等价于 `${argument}`

注意,String(argument)argument + '' 都不能完全等价于 ToStringString() 会把 Symbol 转成字符串描述,而 ToString 对 Symbol 会抛异常。加法运算符转换字符串时还会调用其它函数,比如 argument[Symbol.toPrimitive]
Type ( argument ) (规范)

返回 argument类型

索引

本规范定义的术语

引用定义的术语

参考文献

参考性文献

[CONSOLE]
Dominic Farolino; Terin Stock; Robert Kowalski. Console Standard. 现行标准. URL: https://console.spec.whatwg.org/
[DOM]
Anne van Kesteren. DOM 标准. 现行标准. URL: https://dom.spec.whatwg.org/
[ECMA-262]
ECMAScript 语言规范. URL: https://tc39.es/ecma262/
[ECMA-262-2019]
ECMAScript 2019 语言规范. URL: https://ecma-international.org/ecma-262/10.0/
[HTML]
Anne van Kesteren; 等. HTML 标准. 现行标准. URL: https://html.spec.whatwg.org/multipage/
[ISO-22275-2018]
ISO/IEC 22275:2018 - 信息技术 — 编程语言、其环境和系统软件接口 — ECMAScript® 规范套件. URL: https://www.iso.org/standard/73002.html
[JOHNNY-FIVE]
Johnny-Five: JavaScript 机器人与物联网平台. URL: http://johnny-five.io/
[MDN]
Mozilla 开发者网络. 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]
我用的是什么浏览器?. URL: https://www.whatsmybrowser.org/
[XKCD-703]
Randall Munroe. xkcd: 荣誉学会. URL: https://www.xkcd.com/703/
[YDKJS]
Kyle Simpson. 你不了解 JS(书系列). URL: https://github.com/getify/You-Dont-Know-JS

问题索引

待补充。<https://github.com/TimothyGu/es-howto/issues/2>