如何阅读 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 规范使用术语 记录来指代具有固定键集合的键值映射——有点类似于 类 C 语言中的结构体。记录的每个键值对都称为一个 字段。由于记录只能出现在规范中,而不能出现在实际的 JavaScript 代码中,因此使用 [[Notation]] 来引用字段,是很合理的记录

值得注意的是,属性描述符也是用 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,拥有三种可能的 字段

注: 两个方括号用于表示字段,这些字段属于记录。有关记录 及其相关记法的入门介绍,请参阅 § 2.3.1 记录的字段

完成记录 如果其 [[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 对象是这些异质对象的一种。Array 对象的 length 属性所具有的一些特殊语义,无法使用普通对象可用的机制来实现。

其中之一是这样一个事实:设置一个 Array 对象的 length 属性可以从该对象中移除属性,但 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 对应的行中(也就是我们传递给 substring()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");
  }
});
// 规范堆栈跟踪:
//   ToPrimitive 第 2.e.iii 步。

String({
  [Symbol.toPrimitive]() {
    return { "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,则使用当前领域中的 current realm 的 %ArrayPrototype%。如果 Array 构造函数及其所有属性都未被猴子补丁修改,并且未指定 proto,或者 proto%ArrayPrototype%,则等价于 new Array(length),其中该 %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 转换为 Object。等价于 V[P]

HasOwnProperty ( O, P ) (规范)

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

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

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

IsArray ( argument ) (规范)

返回 argument 是否为一个 Array 异质对象;或者, 如果 argument 是一个 Proxy 异质对象,则返回 argument 最内层 [[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 ) (规范)

返回与 String value 对应的装箱 String Object,并且所得对象的 [[Prototype]] 内部槽为 prototype。如果 prototype当前领域%StringPrototype%,则等价于 new String(value)

ToBoolean ( argument ) (规范)

返回强制转换为 Boolean 的 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 ) (规范)

返回强制转换为 Number 的 argument。等价于 +argument

ToObject ( argument ) (规范)

返回强制转换为 Object 的 argument,必要时使用装箱的原始对象。 除 argumentundefinednull 的情况外,等价于 Object(argument)

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

返回强制转换为原始(即非 Object)值的 input,可以选择使用通过 PreferredType 给出的类型提示。此抽象 操作的确切语义会因 PreferredType 而异。

ToString ( argument ) (规范)

返回强制转换为 String 的 argument。等价于 `${argument}`

必须注意,String(argument)argument + '' 都不完全等价于 ToStringString() 会将 Symbol 值转换为其 String 描述,而 ToString 遇到 Symbol 时会抛出 异常。加法运算符在尝试将值转换为 String 时会调用一些其他函数,例如 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>