重新打造规范

使用可执行、可测试的规范来表达 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 页)
1ScopeScope
2ConformanceConformance
3Normative ReferencesNormative References
4OverviewOverview
5ConventionsNotational Conventions
6Source TextECMAScript Data Types and Values
7Lexical ConventionsAbstract Operations
8TypesExecutable Code and Execution Contexts
9Type Conversion and TestingOrdinary and Exotic Object Behaviors
10Executable Code and Execution ContextsECMAScript Language: Source Code
11ExpressionsECMAScript Language: Lexical Grammar
12StatementsECMAScript Language: Expressions
13Function DefinitionECMAScript Language: Statements and Declarations
14ProgramECMAScript Language: Functions and Classes
15Standard Built-in ECMAScript ObjectsECMAScript Language: Scripts and Modules
16ErrorsError Handling and Language Extensions
17ECMAScript Standard Built-in Objects
18The Global Object
19Fundamental Objects
20Numbers and Dates
21Text Processing
22Indexed Collections
23Keyed Collections
24Structured Data
25Control Abstraction Objects
26Reflection

图 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 特性中引入了更多种类的早期错误。例如,试图使用 letconst 声明来重复定义一个标识符,就属于早期错误。ES6 在语法中增加了「静态语义」(Static Semantic)子条目,用于一致地指定早期错误的触发条件。图 43 显示了一组早期错误定义的示例。如图所示,早期错误规则可以引用静态语义算法。静态语义算法使用与运行时算法相同的约定,只是它们可能不会引用 ECMAScript 环境的任何运行时状态——因为它们是在求值脚本之前应用的。这些静态语义早期错误规则和算法,仅限于使用和分析可从源代码中提取的信息,而无需执行源代码。运行时算法中可以调用静态语义算法,但静态语义算法不能调用运行时算法。

  1. 13.3.1.1 静态语义: Early Errors
  2. LexicalDeclaration : LetOrConst BindingList ;
  3. * 如果 BindingList BoundNames 包含 "let",属于 Syntax Error
  4. * 如果 BindingList BoundNames 包含重复项,属于 Syntax Error
  5. LexicalBinding : BindingIdentifier Initializer (opt)
  6. * 如果 Initializer 不存在,且包含这条产生式的 LexicalDeclaration 对应的 IsConstantDeclaration 结果为 true,属于 Syntax Error
  7. ...
  8. 13.3.1.3 静态语义: IsConstantDeclaration
  9. LexicalDeclaration : LetOrConst BindingList ;
  10. 1. 返回 LetOrConst IsConstantDeclaration
  11. LetOrConst : let
  12. 1. 返回 false
  13. LetOrConst : const
  14. 1. 返回 true

图 43. ES6 静态语义规则示例 [Wirfs-Brock 2015a, pages 194-195]。