共计 16096 个字符,预计需要花费 41 分钟才能阅读完成。
翻译自:How to Read the ECMAScript Specification
Ecmascript 语言规范 The ECMAScript Language specification
(又名:Javascript 规范 the JavaScript specification
或 ECMA-262
)是学习 JavaScript 底层工作原理的非常好的资源。然而,这是一个庞大的专业文本资料,咋一眼看过去,大家可能会感到迷茫、恐惧,满怀激情却无从下手。
前言
不管你是打算每天阅读一点 ECMAScript 规范,还是把它当成一个年度或者季度的目标,这篇文章旨在让你更轻松的开始阅读最权威的 JavaScript 语言参考资料。
为什么要阅读 ECMAScript 规范
Ecmascript 规范是所有 JavaScript 运行行为的权威来源,无论是在你的浏览器环境,还是在服务器环境(Node.js),还是在宇航服上[NodeJS-NASA],或在你的物联网设备上[JOHNNY-FIVE]。所有 JavaScript 引擎的开发者都依赖于这个规范来确保他们各种天花乱坠的新特性能够其他 JavaScript 引擎一样,按预期工作。
Ecmascript 规范 绝不仅仅对 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 {}
并且非常困惑为什么一个方法在它的原型上工作,但是另一个方法在它的原型上却不工作。不幸的是,这种问题你 Google 不到,Stack Overflow 可能也解决不了你的疑惑。
这个时候你就应该去阅读 Ecmascript 语言规范。
或者,您可能想知道臭名昭著的 loose equality operator (==) 是如何工作的(这里松散地使用了单词 `”function”[ WAT])。作为一个勤奋好学的程序员,你在 MDN 上查找它 paragraphs of explanation,却发现上面的解释让你一脸懵逼。
这个时候你就应该去阅读 Ecmascript 语言规范。
另一方面,我不建议初次接触 JavaScript 的开发人员阅读 ECMAScript 规范。如果你是 JavaScript 的新手,那么就开开心心的玩玩 web 吧!开发一些 web 应用,或者一些基于 Javascript 的摄像头等。当你踩了足够多的 JavaScript 坑,或者 JavaScript 问题已经无法再限制你的开发能力的时候,你就可以考虑回到这个文档了。
好了,你现在已经知道,JavaScript 规范在帮助您理解语言或平台的复杂性方面非常有用。但是 ECMAScript 规范究竟包含哪些东西呢?
什么属于 ECMAScript 规范,什么不属于
教科书对这个问题的回答是 “ECMAScript 规范中只包含语言特性 ” 但这并没有帮助,因为这就像是在说 “JavaScript 特性就是 JavaScript” 我不喜欢重言式[XKCD-703]。
相反,我要做的是列出一些在 JavaScript 应用程序中常见的东西,并告诉你它们是否是一个语言特性。
特性 | 是否属于 |
---|---|
Syntax of syntactic elements (i.e., what a valid for..in loop looks like) | Y |
Semantics of syntactic elements (i.e., what typeof null, or { a: b} returns) | Y |
import a from ‘a’; | ? [1] |
Object, Array, Function, Number, Math, RegExp, Proxy, Map, Promise, ArrayBuffer, Uint8Array, … | Y |
console, setTimeout(), setInterval(), clearTimeout(), clearInterval() | N [2] |
Buffer, process, global* | N [3] |
module, exports, require(), __dirname, __filename | N [4] |
window, alert(), confirm(), the DOM (document, HTMLElement, addEventListener(), Worker, …) | N [5] |
- 规范指定了这些声明的语法以及它们的意思,但没有指定如何加载模块。
- 这些东西可以在浏览器和 Node.js 中使用,但不是标准的。对于 Node.js,它们有对应的 Nodejs 文档。对于浏览器,
console
由 Console 标准 定义,而其余部分由 HTML 标准 指定。 - 这些都是 Node.js 仅支持的全局变量,由 Nodejs 文档 定义。* global 实际上有机会成为 ECMAScript 的一部分,并在浏览器中实现 ECMA-262-GLOBAL。
- 这些是仅包含 node.js 模块的 ”globals”,由 其文档 记录 / 指定。
- 这些都是浏览器专用的东西。
在哪里查看 ECMAScript 规范?
当你在 Google “ECMAScript specification” 时,你会看到很多结果,都声称是合法的规范。你应该读哪一本?
更有可能的是,在 tc39.github.io/ecma262/
上发布的规范正是您想要的 ECMA-262。
长话短说:
Ecmascript 语言规范是由一群来自不同背景的人开发的,他们被称为 ECMA 国际技术委员会 39(或者他们更熟悉的名字 TC39[ TC39])。TC39 在 TC39.github.io
[ECMA-262]维护 ECMAScript 语言的最新规范。
让事情变得复杂的是,每年 TC39 都会选择一个时间点对规范进行快照,以便成为当年的 ECMAScript 语言标准,并附带一个版本号。例如,ECMAScript 2018 语言规范 (ECMA-262,第 9 版) ECMA-262-2018 仅仅是 2018 年 6 月在 tc39.github.io
[ECMA-262] 中看到的规范,放入归档库中,并进行适当地包裹和 PDFified 以供永久存档。
正因为如此,除非你希望你的 web 应用程序从 2018 年 6 月开始只能运行在放入甲醛、适当包装和 PDFified 以便永久存档的浏览器上,否则你应该始终查看 tc39.github.io
[ECMA-262]的最新规范。但是,如果您希望 (或者必须) 支持旧版本的浏览器或 Node.js 版本,那么引用旧版本的规范可能会有所帮助。
注: iso/iec 也将 ECMAScript 语言标准发表为 iso/iec16262[ISO-16262-2011]。不过不用担心,因为这个标准的文本和 ECMA 国际出版的标准文本完全一样,唯一的区别是你必须支付 198 瑞士法郎。
Navigating the spec 规范导航
Ecmascript 规范谈论了大量的内容。尽管它的作者尽最大努力把它分割成小的逻辑块,它仍然是一个超级庞大的文本。
就我个人而言,我喜欢把规格分为五个部分:
- Conventions and basics 约定和基础 (什么是数字? 当规范说
throw a TypeError exception
是什么意思?) - Grammar productions of the language 语言的语法结果 (如何编写
for-in
循环?) - Static semantics of the language 语言的静态语义 (
var
语句中如何确定变量名称?) - Runtime semantics of the language 语言的运行时语义 (
for-in
循环是如何执行的?) - APIs (
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 The
if
Statement Grammar productions
- §13.6.1-6 Static semantics
- §13.6.7 Runtime sematics
§13.7 Iteration Statements Grammar productions
- §13.7.1 Shared static and runtime semantics - §13.7.2 The `do-while` Statement - §13.7.2.1-5 Static semantics - §13.7.2.6 Runtime semantics
§13.7.3 The
while
Statement…
而 APIs 则通过 §18 The Global Object 通过 §26 Reflection 扩展全局对象。
在这一点上,我想指出的是,绝对没有人从上到下阅读规范 。相反,只看与你要查找的内容相对应的部分,在该部分中只看您需要的内容。试着确定你的具体问题涉及的五大部分中的哪一部分; 如果你无法确定哪一部分是相关的,问问自己这样一个问题:” 在什么时候(无论你想要确认什么) 这个问题被评估了?” 这可能会有帮助。不要担心,操作规范只会在实践中变得更容易。
Runtime semantics 运行时语义
Runtime semantics of The Language
和 APIs
的运行时语义是规范中最重要的部分,通常是人们最关心的问题。
总的来说,在规范中阅读这些章节是非常简单的。然而,该规范使用了大量的人才,这些人才刚刚开始 (至少对我来说) 是相当讨厌的。我将尝试解释其中的一些约定,然后将它们应用到一个通常的工作流程中,以弄清楚几件事情是如何工作的。
Algorithm steps 算法步骤
Ecmascript 中的大多数 runtime semantics
(运行时语义)是由一系列 algorithm steps
(算法步骤)指定的,与伪代码没有什么不同,但形式精确得多。
A sample set of algorithm steps are:
- Let a be 1.
- Let b be a+a.
If b is 2, then
- Hooray! Arithmetics isn’t broken.
Else
- Boo!
拓展阅读:§5.2 Algorithm Conventions
Abstract operations 抽象操作
你有时会看到类似函数的东西在 spec 中被调用。Boolean() 函数的第一步是:
当使用参数值调用 Boolean 时,将执行以下步骤:
- Let b be ToBoolean(value).
- …
这个 ToBoolean
函数被称为 abstract operation (抽象操作):它是抽象的,因为它实际上并没有作为一个函数暴露给 JavaScript 代码。它只是一个 notation spec writers
(符号规范作者)发明的,让他们不要写同样的东西一遍又一遍。
拓展阅读:§5.2.1 Abstract Operations
What is [[This]]
有时候,你可能会看到 [[Notation]] 被用作 “Let proto be obj.[[Prototype]]”。这个符号在技术上可能意味着几个不同的东西,这取决于它出现的上下文,但是你可以理解,这个符号指的是一些不能通过 JavaScript 代码观察到的内部属性。
准确地说,它可以意味着三种不同的东西,我将用规范中的例子来说明它们。不过,你可以暂时跳过它们。
A field of a Record
Ecmascript 规范使用术语 Record
来引用具有固定键集的 key-value map
——有点像 C 语言中的结构。Record 的每个 key-value
对称为 field
。因为 Records 只能出现在规范中,而不能出现在实际的 JavaScript 代码中,所以使用 [[Notation]]
来引用 Record 的 field 是有意义的。
Notably, Property Descriptors are also modeled as Records with fields [[Value]], [[Writable]], [[Get]], [[Set]], [[Enumerable]], and [[Configurable]]. The IsDataDescriptor abstract operation uses this notation extensively:
When the abstract operation IsDataDescriptor is called with Property Descriptor Desc, the following steps are taken:
- If Desc is undefined, return false.
- If both Desc.[[Value]] and Desc.[[Writable]] are absent, return false.
- Return true.
另一个 Records 的具体例子可以在下一节中找到, §2.4 Completion Records; ? and !
拓展阅读: §6.2.1 The List and Record Specification Types
An internal slot of a JavaScript Object
Javascript 对象可能有所谓的 internal slots,规范使用这些 internal slots 来保存数据。像 Record 字段一样,这些 internal slots 也不能用 JavaScript 观察到,但是其中一些可能会通过特定于实现的工具 (如 Google Chrome 的 DevTools) 暴露出来。因此,使用 [[Notation]]
来描述 internal slots 也是有意义的。
internal slots 的细节将在 §2.5 JavaScript Objects 中介绍。现在,不要过于担心它们的用途,但请注意下面的例子。
Most JavaScript Objects have an internal slot [[Prototype]] that refers to the Object they inherit from. The value of this internal slot is usually the value that Object.getPrototypeOf() returns. In the OrdinaryGetPrototypeOf abstract operation, the value of this internal slot is accessed:
When the abstract operation OrdinaryGetPrototypeOf is called with Object O, the following steps are taken:
- Return O.[[Prototype]].
注意: Object 和 Record fields 的 Internal slots 在外观上是相同的,但是可以通过查看这个符号 (点之前的部分) 的先例来消除它们的歧义,无论它是 Object 还是 Record。从周围的环境来看,这通常是相当明显的。
An internal method of a JavaScript Object
Javascript 对象也可能有所谓的 internal methods。像 internal slots 一样,这些 internal methods 不能通过 JavaScript 直接观察到。因此,使用 [[Notation]]
来描述 internal methods 也是有意义的。
internal methods 的细节将在 §2.5 JavaScript Objects 中介绍。现在,不要过于担心它们的用途,但请注意下面的例子。
All JavaScript functions have an internal method [[Call]] that runs that function. The Call abstract operation has the following step:
- Return ? F.[[Call]](V, argumentsList).
where F is a JavaScript function object. In this case, the [[Call]] internal method of F is itself called with arguments V and argumentsList.
注意: [[[Notation]]的第三种意义可以通过看起来像一个函数调用来区分。
Completion Records; ? and !
Ecmascript 规范中的每个运行时语义都显式或隐式地返回一个报告其结果的 Completion Record
。这个 Completion Record
是一个 Record,有三个可能的领域:
- 一个 [[Type]] (normal, return, throw, break, 或 continue)
- 如果 [[Type]] 是正常的, return, or throw, 那么它也可以有
[[Value]]
(“what’s returned/thrown”) - 如果[[Type]] 是中断或继续, 那么它可以选择带有一个称为
[[Target]]
的标签,由于运行时语义的原因,脚本执行 breaks from/continues
A Completion Record
whose [[Type]] is normal
is called a normal completion
. Every Completion Record
other than a normal completion
is also known as an abrupt completion
.
大多数情况下,您只需要处理 abrupt completions 的 [[Type]]
是 throw。其他三种 abrupt completion 类型仅在查看如何评估特定语法元素时有用。实际上,在内置函数的定义中,您永远不会看到任何其他类型,因为 break / continue / return 不跨函数边界工作。
拓展阅读:§6.2.3 The Completion Record Specification Type
由于 Completion Records 的定义,JavaScript 中的细节 (如冒泡错误,直到 try-catch
块) 在规范中不存在。事实上,错误 (或更准确地说是 abrupt completions) 是显式处理的。
没有任何缩写,对抽象操作的普通调用的规范文本可能会返回一个计算结果或抛出一个错误,它看起来像:
下面是一些调用抽象操作的步骤,这些步骤可以抛出
without any shorthands
的操作:
- Let resultCompletionRecord be AbstractOp().
Note: resultCompletionRecord is a Completion Record.
- If resultCompletionRecord is an abrupt completion, return resultCompletionRecord.
注意: 在这里,如果是 abrupt completion,则直接返回 resultCompletionRecord。换句话说,会转发 AbstractOp 中抛出的错误,并终止其余的步骤。
- Let result be resultCompletionRecord.[[Value]].
注意: 在确保得到 normal completion 后,我们现在可以解构 Completion Record 以得到我们需要的计算的实际结果。
- result 就是我们需要的结果。我们现在可以用它做更多的事情。
这可能会模糊地让你想起 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 以来,同样的规范文本可以用以下两种等价的方式编写:
下面的几个步骤可以调用一个抽象操作,这个操作可能会抛出
ReturnIfAbrupt
:
- Let result be AbstractOp().
注意: 这里,就像前面例子中的步骤 1 一样,结果是一个 Completion Record.
- ReturnIfAbrupt(result).
Note: 注意:
ReturnIfAbrupt
通过转发来处理任何可能出现的 abrupt completions,并自动将 result 解构到它的[[Value]]
- result 就是我们需要的结果。我们现在可以用它做更多的事情。
或者,更准确地说,用一个特殊的问号 (?)
符号:
调用可能带有问号 (?) 的抽象操作的几个步骤 :
- Let result be ? AbstractOp().
注意:在这个 notation 中,我们根本不处理
Completion Records
。?
shorthand 为我们处理了一切事情, 且 result 立马就可用
- result 就是我们需要的结果。我们现在可以用它做更多的事情。
有时,如果我们知道对 AbstractOp 的特定调用永远不会返回一个 abrupt completion,那么它可以向读者传达更多关于 spec’s intent
。在这些情况下,一个 exclamation mark (!)
用于:
A few steps that call an abstract operation that cannot ever throw with an exclamation mark (!):
Let result be ! AbstractOp().
Note: While ? forwards any errors we may have gotten, ! asserts that we never get any abrupt completions from this call, and it would be a bug in the specification if we did. Like the case with ?, we don’t deal with Completion Records at all. result is ready to use immediately after.
result is the result we need. We can now do more things with it.
拓展阅读:§5.2.3.4 ReturnIfAbrupt Shorthands
JavaScript Objects
在 ECMAScript 中,每个 Object 都有一组内部方法,规范的其余部分调用这些方法来完成某些任务。所有 object 都有的一些内部方法是:
- [[Get]], which gets a property on an Object (e.g. obj.prop)
- [[Set]], which sets a property on an Object (e.g. obj.prop = 42;)
- [[GetPrototypeOf]], which gets the Object’s prototype (i.e., Object.getPrototypeOf(obj))
- [[GetOwnProperty]], which gets the Property Descriptor of an own property of an Object (i.e., Object.getOwnPropertyDescriptor(obj, “prop”))
- [[Delete]], which deletes a property on an Object (e.g. delete obj.prop)
详尽的列表可在 §6.1.7.2 Object Internal Methods and Internal Slots 中找到。
基于这个定义,function objects (or just “functions”) 是简单的对象,它们附加了 [[Call]] 内部方法,可能还有 [[Construct]] 内部方法; 因此,它们也被称为 callable objects。
然后,规范将所有 Object 分为两类: ordinary objects
和 exotic objects
。你遇到的大多数对象都是 ordinary objects,这意味着它们所有的内部方法都是在 §9.1 Ordinary Object Internal Methods and Internal Slots。
然而,ECMAScript 规范还定义了一些 exotic objects,这些对象可以覆盖这些内部方法的默认实现。对于允许外来对象执行的操作,有一定的最小限制,但是一般来说,过多的内部方法可以执行大量的特技操作,而不违反规范。
Array 对象是这些 exotic objects 的一种。使用 ordinary objects 可用的工具无法获取像 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 Array Exotic Objects
Javascript 对象也可能有定义为包含特定类型值的 internal slots。我倾向于将 internal slots 视为甚至对 Object.getOwnPropertySymbols()
都隐藏的以符号命名的属性。ordinary objects 和 exotic objects 都允许有 internal slots。
在 An internal slot of a JavaScript Object
中, 我提到了大多数对象都具有的一个名为 [[Prototype]]
的 internal slot
。(事实上,所有的 ordinary objects,甚至一些 exotic objects,比如 Array 对象都有它。) 但是我们也知道有一个叫[[GetPrototypeOf]]
的内部方法,我在上面简要描述过。它们之间有什么区别吗?
这里的关键字是 most
: 虽然大多数对象都有 [[Prototype]]
internal slot,但所有对象都实现 [[GetPrototypeOf]]
内部方法。值得注意的是,Proxy 对象没有它们自己的 [[Prototype]]
,而且它的 [[GetPrototypeOf]]
内部方法遵从已注册的处理程序或其目标的原型,存储在 Proxy 对象的 [[ProxyTarget]]
的 internal slot 中。
因此,在处理对象时,引用适当的 internal method
几乎总是一个好主意,而不是直接查看 internal slot
的值。
另一种思考 Objects
, internal methods
和 internal slots
之间关系的方式是通过经典的 object-oriented lens
。“Object” 类似于指定必须实现的几个 internal methods
的接口。ordinary objects
提供了缺省实现,exotic objects
可以部分或全部覆盖。另一方面,internal slots
似于 Object
的实例变量 —— Object 的实现细节。
所有这些关系都可以用下面的 UML 图来概括:
示例: String.prototype.substring()
现在我们已经很好地理解了规范是如何组织和编写的,让我们开始练习吧!
假设我现在有以下问题:
如果不运行代码,下面的代码片段返回什么?
String.prototype.substring.call(undefined, 2, 4)
这是一个相当棘手的问题。似乎有两种看似合理的结果:
- String.prototype.substring() 可以首先将
undefined
强制转换为"undefined"
字符串,然后在该字符串的第二和第三个位置 (即间隔 [2,4] ) 获取字符得到"de"
。 - 另一方面,
String.prototype.substring()
也可以合理地抛出一个错误,从而拒绝未定义的输入。
不幸的是,当 this 的值不是字符串时,MDN 也没有真正提供任何关于函数运行的说明。
在阅读 algorithm steps 之前,让我们先想想我们知道什么。我假设我们已经对 str.substring()
的通常工作方式有了基本的了解:即返回给定字符串的一部分。我们现在真正不确定的是,在 this
值为 undefined
的情况下,它是如何运作的。因此,我们将特别寻找解决 this 值的 algorithm steps
。
幸运的是,String.prototype.substring() algorithm 的第一步专门处理这个值:
- Let O be ? RequireObjectCoercible(this value).
?
shorthand 允许我们得出这样的结论: 在某些情况下,RequireObjectCoercible 抽象操作实际上可能会抛出异常,因为否则 !
会被用来代替。事实上,如果它抛出一个错误,它将与我们上面的第二个假设相对应!有了希望,我们可以通过单击超链接来了解 RequireObjectCoercible 做了什么。
Requireobjectforecble
抽象操作有点奇怪。与大多数抽象操作不同,它是通过表格而不是步骤来定义的:
Argument Type | Result |
---|---|
Undefined 的 | Throw a TypeError exception |
… | … |
不管怎样——在对应于 Undefined
(我们传递给 substring()
的 this 值的类型)的行中,规范说 RequireObjectCoercible 应该抛出一个异常。那是因为在 函数的定义中使用 ?
,我们知道抛出的异常必须冒泡到函数的调用方。
这就是我们的答案: 给定的代码片段会抛出一个 TypeError
异常。
规范只指定了错误抛出的类型,而没有指定它包含的消息。这意味着实现可以有不同的错误消息,甚至是本地化的错误消息。
例如,在谷歌的 v86.4(包含在谷歌 Chrome 64 中)上,消息是:
TypeError: String.prototype.substring called on null or undefined
而 Mozilla Firefox 57.0 提供了一些不那么有用的功能
TypeError: can’t convert undefined to object
与此同时,ChakraCore version 1.7.5.0(Microsoft Edge 中的 JavaScript 引擎)采用了 V8 的路线并抛出
TypeError: String.prototype.substring: 'this' is null or undefined
示例: Boolean() 和 String() 会抛出异常吗?
在编写关键任务代码时,必须优先考虑异常处理。因此,” 某个内置函数会抛出异常吗?” 需要仔细琢磨。
在本例中,我们将尝试回答两个语言内置函数 Boolean()
和 String()
的问题。我们将只关注对这些函数的直接调用,而不是 new Boolean()
和 new String()
——这是 JavaScript 中最不受欢迎的特性 之一,也是几乎所有 JS 编程指南中最不鼓励的实践 YDKJS。
在找到规范中的 Boolean() 部分之后,我们看到算法似乎相当简短:
当使用参数值调用 Boolean 时,将执行以下步骤:
- Let b be ToBoolean(value).
- If NewTarget is undefined, return b.
- Let O be ? OrdinaryCreateFromConstructor(NewTarget,
"%BooleanPrototype%"
, « [[BooleanData]] »).- Set O.[[BooleanData]] to b.
- Return O.
但是从另一方面来说,这并不是完全简单的,在 OrdinaryCreateFromConstructor 这里涉及到一些复杂的基本技巧。更重要的是,有一个 ?
步骤 3 中的简写,可能表明此函数在某些情况下可能抛出错误。让我们仔细看看。
步骤 1 将 value (函数参数)转换为布尔值。有趣的是,没有一个 ?
或者 !
这一步骤的 shorthand,但通常没有 Completion Record shorthand 等同于 !
. 因此,步骤 1 不能抛出异常。
第 2 步检查名为 NewTarget 的东西是否 undefined。Newtarget
是在 ES2015 中首次添加的 new.target 元属性的 spec 等价物,它允许规范区分 new Boolean()
调用。因为我们现在只关注对 Boolean()
的直接调用,所以我们知道 NewTarget
总是未定义的,并且算法总是直接返回 b,而不需要任何额外的处理。
因为调用不带 new
的 Boolean()
只能访问 Boolean()
算法中的前两个步骤,而这两个步骤都不能引发异常,所以我们得出结论: 无论输入是什么,Boolean()
都不会引发异常。
让我们把注意力转向 String () :
当使用参数值调用 String 时,将执行以下步骤:
- If no arguments were passed to this function invocation, let s be “”.
Else,
- If NewTarget is undefined and Type(value) is Symbol, return SymbolDescriptiveString(value).
- Let s be ? ToString(value).
- If NewTarget is undefined, return s.
- Return ? StringCreate(s, ? GetPrototypeFromConstructor(NewTarget,
"%StringPrototype%"
)).
根据我们使用 Boolean() 函数进行同类分析的经验,我们知道对于我们的案例,NewTarget 总是未定义的,因此可以跳过最后一步。我们还知道 Type 和 SymbolDescriptiveString 也是安全的,因为它们都不会处理 abrupt completions。然而,还有一个关于 ?
的问题吗?在调用 ToString 抽象操作之前。让我们仔细看看。
就像我们前面看到的 RequireObjectCoercible 一样,ToString(argument) 也是用一个表定义的:
Argument Type | Result |
---|---|
Undefined | Return “undefined” |
Null | Return “null” |
Boolean | If argument is true, return “true” If argument is false, return “false” |
Number | Return NumberToString(argument) |
String | Return argument |
Symbol | Throw a TypeError exception |
Object | Apply the following steps: 1. Let primValue be ? ToPrimitive(argument, hint String) 2. Return ? ToString(primValue) |
在 String() 中调用 ToString 时,value 可以是 Symbol
以外的任何值(在紧接着的步骤中过滤掉)。然而,还有两个 ?
对象行中的。我们可以点击 ToPrimitive 和 beyond 的链接,发现如果 value 是 Object,那么实际上有很多机会抛出错误:
所以对于 String()
,我们的结论是它从不为 primitive values
抛出异常,但可能为 Objects 抛出错误。
更多关于 String() throws 的例子如下:
// Spec stack trace:
// OrdinaryGet step 8.
// Ordinary Object’s [[Get]]() step 1.
// GetV step 3.
// GetMethod step 2.
// ToPrimitive step 2.d.
String({get [Symbol.toPrimitive]() {throw new Error("Breaking JavaScript");
}
});
// Spec stack trace:
// GetMethod step 4.
// ToPrimitive step 2.d.
String({get [Symbol.toPrimitive]() {return "Breaking JavaScript";}
});
// Spec stack trace:
// ToPrimitive step 2.e.i.
String({[Symbol.toPrimitive]() {throw new Error("Breaking JavaScript");
}
});
// Spec stack trace:
// ToPrimitive step 2.e.iii.
String({[Symbol.toPrimitive]() {return { "breaking": "JavaScript"};
}
});
// Spec stack trace:
// OrdinaryToPrimitive step 5.b.i.
// ToPrimitive step 2.g.
String({toString() {throw new Error("Breaking JavaScript");
}
});
// Spec stack trace:
// OrdinaryToPrimitive step 5.b.i.
// ToPrimitive step 2.g.
String({valueOf() {throw new Error("Breaking JavaScript");
}
});
// Spec stack trace:
// OrdinaryToPrimitive step 6.
// ToPrimitive step 2.g.
String(Object.create(null));
示例:typeof operator
到目前为止,我们只分析了 API 函数,让我们尝试一些不同的东西。
未完待续 https://github.com/TimothyGu/…
参考
- How to Read the ECMAScript Specification
- ECMAScript® 2020 Language Specification
- JavaScript 深入之从 ECMAScript 规范解读 this
- 读懂 ECMAScript 规格