重新打造规范
使用可执行、可测试的规范来表达 ECMAScript 语义的愿望,从新版 ES4 的工作中延续了下来。但使用 ML 作为规范语言的尝试已经被放弃了。在 Harmony 工作的早期,Allen Wirfs-Brook [2009] 提出了通过「以 ES5 JavaScript 编写的定义解释器」来确定 Harmony 的想法。这个想法甚至被列入了 Harmony 目标声明中(图 38)。但到 2010 年春天,在这个概念上仍然没有取得什么进展,TC39 成员对此方法也感到了更多的不确定性。而为 ES5(附录 P)所做的伪代码改进,已经消除了早期版本中伪代码存在的大部分可用性问题。并且 Test262 的进展也表明,一套全面的测试套件对于验证规范和实现同样有用。在 5 月的 TC39 会议 [2010] 上,人们再次讨论了规范的形式。当前现状对会议上的许多人来说仍然很有吸引力。苹果公司的 Oliver Hunt 发现,作为规范实现者,ES5 中的伪代码比他见过的任何可执行规范代码都更好用。于是会议一致决定继续使用伪代码来定义 Harmony。
对于项目编辑来说,创建规范并不仅仅是一件简单的集成任务。从理论上来说,提案应当由倡导者开发到「可以轻松集成到规范中」的程度。但在实践中,这种情况很少发生。一些倡导者对规范的结构或形式不够熟悉,无法创建可集成的伪代码。另外一些人则没有必要的时间或专业知识来创建详细的语义规范。对于许多提案,Allen Wirfs-Brock 不得不设法将它们集成到规范中。这需要制定语义细节,并编写或重写提案在规范中的算法。
倡导者们往往会较为狭隘地关注自己的提案所定义的特性。好的提案会考虑到该特性如何与语言的现有特性交互。然而即使是最熟练的倡导者,也很难考虑他们的特性和「其他倡导者同时开发的其他提案」之间所有的潜在交互。所有特性都必须通过编辑,才能成为实际规范的一部分。所以 Wirfs-Brock 对于原有语言和所有 Harmony 提案如何结合在一起形成 ES6,有着最完整的看法。他特别关注跨越多个特性提案的交叉问题,并确保提案之间在语法和语义上的一致性。当整合已批准的提案时,他会试图将它们转化为一组可组合的正交特性 [Linsey 1993]。有时,这需要改变提案的语法或语义细节,甚至增加或删除重要特性。然后这些改变必须提交给倡导者,而且往往还要提交给整个委员会批准。
重组规范结构
从 1997 年的第一版初稿(图 13)到 ES5.1 为止,ECMAScript 规范的组织结构基本没有变化。在编写 ES5 规范时,Allen Wirfs-Brook 发现规范中材料的基本排序令人困惑。他逐渐认识到规范实际上定义了三个独立的部分:
- 一个 ECMAScript 虚拟机,包括各种运行时实体及其语义。
- ECMAScript 语言的语法、语义,及其与虚拟机之间的映射。
- 所有 ECMAScript 程序都可以使用的各种标准库对象。
原始规范及其修订版将三部分交织在一起,掩盖了这一基本结构。Allen Wirfs-Brock 认为,将规范明确地组织成三部分结构将使其更容易理解,还能更清楚地介绍大量新的 ES6 材料。委员会对此表示同意。图 42 显示了 ES2015 规范的新组织结构与 ES5 规范之间的比较。
条目 | ECMA-262 第 5.1 版(245 页) | ECMA-262 第 6 版(545 页) |
---|---|---|
1 | Scope | Scope |
2 | Conformance | Conformance |
3 | Normative References | Normative References |
4 | Overview | Overview |
5 | Conventions | Notational Conventions |
6 | Source Text | ECMAScript Data Types and Values |
7 | Lexical Conventions | Abstract Operations |
8 | Types | Executable Code and Execution Contexts |
9 | Type Conversion and Testing | Ordinary and Exotic Object Behaviors |
10 | Executable Code and Execution Contexts | ECMAScript Language: Source Code |
11 | Expressions | ECMAScript Language: Lexical Grammar |
12 | Statements | ECMAScript Language: Expressions |
13 | Function Definition | ECMAScript Language: Statements and Declarations |
14 | Program | ECMAScript Language: Functions and Classes |
15 | Standard Built-in ECMAScript Objects | ECMAScript Language: Scripts and Modules |
16 | Errors | Error Handling and Language Extensions |
17 | ECMAScript Standard Built-in Objects | |
18 | The Global Object | |
19 | Fundamental Objects | |
20 | Numbers and Dates | |
21 | Text Processing | |
22 | Indexed Collections | |
23 | Keyed Collections | |
24 | Structured Data | |
25 | Control Abstraction Objects | |
26 | Reflection |
图 42. 第五版和第六版规范的组织。在 ES6 规范中,第 6-9 条定义了虚拟机语义。第 10-15 条定义了语言,第 17-26 条定义了标准库。
新的术语
ES6 为澄清和更新规范中使用的一些术语提供了机会。其中需要注意的一个领域,就是对象的命名规则。在 JavaScript 1.0 的实现中,JavaScript 程序可以访问特定于宿主和 JavaScript 引擎的对象。这些对象的基本语义,相比于用 ECMAScript 代码所能创建的对象,有着多种不同的区别。ES1 规范中使用了以下术语:「对象」、「原生对象」、「标准对象」、「内置对象」、「标准原生对象」、「内置原生对象」和「宿主对象」,以指代可以实现对象的各种方式。这些称呼之间的区别很微妙,但却没有特别的用处。人们不清楚这些类别中到底哪些允许特别的对象语义,也不清楚 JavaScript 程序员所创建的对象与其中的哪些相匹配。
ES6 的一个目标,是使大多数标准库和宿主对象能使用 JavaScript 代码进行「自托管的实现」。有了自托管的可能性,对象是由宿主提供、由引擎提供还是由程序提供,其中的区别就显得越来越不重要了。对象之间的语义差异,相比于「谁来提供它们」或「实现它们的技术」更为重要。
对此在术语上的基本需求,是区分具有正常语义的对象和具有反常(即不寻常)语义的对象。Douglas Crockford [TC39 2012b] 根据 Ecma 最高会员等级的名称,建议用「标准对象g」来表示那些语义上使用 JavaScript 对象字面量或 new Object()
来创建的对象。凡是在语义上与普通对象语义有任何偏离的对象,都被称为「异质对象g」。标准对象和异质对象都可能由宿主、引擎或应用程序员提供,也可能用 JavaScript 或其他语言来实现。
新的语义种类
在 ES6 之前,除了那些定义标准库函数的算法之外,大多数伪代码算法都与语法产生式相关联,并指定了相应产生式的运行时求值语义。并没有必要对这些算法进行命名,因为它们是唯一与语法产生式相关联的语义。此外还有一些算法(如类型转换的算法和定义对象语义的内部方法)则没有直接与语法相关联。这些算法被赋予了名称,以便于从求值算法中引用。
ES6 引入了形如对象解构之类的新特性,它们具有复杂的行为,其规范必须横贯多种语法产生式。一些算法需要对解析树进行多次遍历以收集信息,或对跨越多个解析节点的求值步骤进行排序。还有一些常见的在语法上存在关联的行为,会为了保持一致性而在多种语言特性之间复用。为适应这些需求,ES6 规范中除了隐式命名的求值算法外,还可以将命名算法与解析节点关联起来。它们通过名称被其所关联的语法符号引用。通常这种命名算法是多态的,即一个同名算法被定义为多种语法产生式。实际选择的具体算法,取决于在解析特定源文本语法符号时所进行的推导。
为了最大限度地减少实现之间的差异,ECMA-262 的每一个后续版本都更精确地定义了错误条件,以及应在何时检测到它们。ES3 隐式地引入了「早期错误」的概念,并在 ES5 中进一步完善。所谓早期错误,指的是在脚本求值前就会被检测到并报告的错误。一旦检测到早期错误,就会阻止对脚本的求值。最常见的早期错误形式是语法错误。当脚本的源代码不能使用 ECMAScript 语法进行解析时,就会出现这种错误。语法错误隐含在了语法的定义中。ES3 引入了一些其他类型的早期错误,例如在 break
语句中引用了语句标签,而相应标签在词法上没有包围住 break
语句时。ES5 严格模式中又增加了一些早期错误。尽管这些错误不属于解析错误,规范还是将大多数此类错误定义为语法错误,即对语言静态语义规则的违反。在 ES6 之前,多数这样的错误都通过位于求值算法附近的非正式叙述来确定,其他则通过使用伪代码来确定。这些伪代码会在求值算法中测试运行时的错误条件,然后基于叙述来说明该错误「可以或应该」作为早期错误报告。
ES6 特性中引入了更多种类的早期错误。例如,试图使用 let
或 const
声明来重复定义一个标识符,就属于早期错误。ES6 在语法中增加了「静态语义」(Static Semantic)子条目,用于一致地指定早期错误的触发条件。图 43 显示了一组早期错误定义的示例。如图所示,早期错误规则可以引用静态语义算法。静态语义算法使用与运行时算法相同的约定,只是它们可能不会引用 ECMAScript 环境的任何运行时状态——因为它们是在求值脚本之前应用的。这些静态语义早期错误规则和算法,仅限于使用和分析可从源代码中提取的信息,而无需执行源代码。运行时算法中可以调用静态语义算法,但静态语义算法不能调用运行时算法。
13.3.1.1 静态语义: Early Errors
LexicalDeclaration : LetOrConst BindingList ;
* 如果 BindingList 的 BoundNames 包含 "let",属于 Syntax Error。
* 如果 BindingList 的 BoundNames 包含重复项,属于 Syntax Error。
LexicalBinding : BindingIdentifier Initializer (opt)
* 如果 Initializer 不存在,且包含这条产生式的 LexicalDeclaration 对应的 IsConstantDeclaration 结果为 true,属于 Syntax Error。
...
13.3.1.3 静态语义: IsConstantDeclaration
LexicalDeclaration : LetOrConst BindingList ;
1. 返回 LetOrConst 的 IsConstantDeclaration。
LetOrConst : let
1. 返回 false。
LetOrConst : const
1. 返回 true。
图 43. ES6 静态语义规则示例 [Wirfs-Brock 2015a, pages 194-195]。