编写规范

Michael Gardner 和 Randy Solton 在 11 月的会议之后,立即开始了制订第一份规范草案的工作,并在接下来的六周中取得了显著进展。除 Gardner 和 Solton 以外,首份草案的技术贡献者还包括如下:Brendan Eich(Netscape)、C. Rand McKinney(Netscape)、Donna Converse(Netscape)、Shon Katzenberger(微软)和 Robert Welland(微软)。

Robert Welland 返回 Redmond 后,将他的 JScript 0.1 规范交接给了 Shon Katzenberger,以继续开发语言语义 [Welland et at. 2018, at +12:02]。数学博士出身的 Katzenberger 对形式化表示法感到满意。他发现伪代码概念(Appendix P)在描述 JavaScript 语义方面相当有效,其详细程度在他眼里是足以确保互操作性的。Katzenberger 成为了微软对标准开发的主要技术贡献者。他将草稿与现有实现相对照,并为未覆盖到的部分附加编写伪代码算法,从而扩展了 Welland 和 Smith 的深夜工作。然后,他将自己修改后的新材料发送给 Borland 的编辑,以纳入正式草案。2018 年 Katzenberger 在接受采访时 [Welland et at. 2018, at +21:16],表示他对编辑过程中的改动有时会无意破坏自己的算法而有所不满。当 Guy Steele 可以担任编辑时,他感到相当高兴。

1 月 10 日的草案 [TC39 1997c] 建立了规范的基本结构(图 13),并确定了用于定义语言的许多基础技术、约定和惯用语。在 20 年后的 ECMAScript 标准版本中,这些概念有许多仍在使用。

1997 年 1 月 10 日的草案ECMA-262 第 1 版
Scope
Conformance
Reference
Overview
Notational ConventionsNotational Conventions
Source TextSource Text
Lexical ConventionsLexical Conventions
TypesTypes
Type ConversionType Conversion
VariablesExecution Contexts
ExpressionsExpressions
StatementsStatements
Function DefinitionFunction Definition
ProgramProgram
Native ECMAScript ObjectsNative ECMAScript Objects
Errors

图 13. ECMAScript 标准的结构。

草案中对语法约定的描述,主要来自 Netscape 的规范。但至于表达式与语句级语法的结构,以及产生式(production)的名称,则在很大程度上遵循了微软规范中的用法。在两份贡献出的规范中,表达式语法在细微的细节层面上有所不同,例如函数调用的优先级、对象的创建(new 运算符),以及对象属性访问表达式的元素等。

这份草案试图将自动分号插入(ASI)的规则,精确地定义为用来「校正解析错误」的过程。语句的语法包括了显式的分号,用于终止所有非复合语句。如果没有 ASI,那么缺失分号将会产生解析错误。ASI 规范定义了 JavaScript 解析器何时必须通过「假设存在分号」并重新解析的方式,来尝试纠正此类解析错误。第一版草案中的 ASI 规则并不完整,这在后来的 ECMAScript 规范草案和发行版中进行了完善。

1 月 10 日的草案中包含了 Shon Katzenberger 的伪代码算法(例如图 14),用于定义各种语言结构的语义。具体算法的组成,则包括了带顺序编号的步骤,以及步骤之间的简单条件控制流。每个步骤都包含一些命令式g的叙述。步骤的叙述用英语编写,并结合了规范中针对常见动作所定义的基本词汇。可以在规范内的其他算法中命名和「调用」这些算法。

  1. 4.4.7 GetValue(V)
  2. 1. If Type(V) is not a Reference, return V.
  3. 2. Call GetBase(V)
  4. 3. If Result(2) is null, generate a runtime error.
  5. 4. Call the [[Get]] method of Result(2), passing GetProperty(V) for the property name and GetAccess(V) for the access mode.
  6. 5. Return Result(4).

图 14. 在 2007 年 1 月 10 日的 ECMAScript 规范中 [TC39 1997c, §4.7.4],一个具名的伪代码算法。原始文档中的步骤 2 末尾少了一个句号。

草案还定义了算法中使用的数据类型。ECMAScript 程序中可见值的类型包括 Number、Boolean、String、Object、Undefined 和 Null。另外还有 Reference、Completion 和 List 类型的值用于定义语言语义,ECMAScript 程序无法直接接触到它们。

对象类型的规范引入了属性标记g的概念,用于控制如何访问或修改各个属性。规范共定义了七种不同的标记:ReadOnly、ErrorOnWrite、DontEnum、NotImplicit、NotExplicit、Permanent 和 Internal。最后,ErrorOnWrite、NotImplicit 和 NotExplicit 被移除,而 Permanent 则被重命名为 DontDelete。具有 Internal 标记的属性会保留与对象相关联的内部状态,但这对 ECMAScript 程序并不直接可见。这种内部属性g的用途是保存状态。对于实现对象语义,或者实现内置对象与宿主对象的唯一行为,这些状态都是必需的。

一并引入的概念还包括内部方法g,这是用于定义对象基本行为的算法。对于某些内部方法,可以用替代性的定义来指定不同种类的对象(例如 Array 对象),从而支持它们在行为上的变化。内部方法的接口,实质上是简单元对象协议g的规范 [Kiczales et al. 1991]。

在规范中,内部方法和内部属性的名称被括在双括号中,形如 [[Foo]]。1 月 10 日的草案定义了内部方法 [[Get]][[Put]][[HasProperty]][[Construct]][[Call]] 和内部属性 [[Prototype]]。在第一次形式化表达对象属性访问、原型继承和函数调用的语义时,用到的就是这些内部方法。到 ES1 完成时,又添加了 [[CanPut]][[Delete]] 内部方法。

第一稿的目录中既包含了原生(内置)ECMAScript 对象,也包含了由浏览器和 Web 服务端宿主环境提供的对象。但是这些部分在 1 月 10 日的草案中仍然留空。草案中有 20 个条目被明确标记为「问题」,它们是许多附录中描述的潜在语言扩展的补充。

1 月 10 日的草案,是 1997 年 1 月 15 日首次技术工作组会议上讨论的基础。会议做出了一些重要的决定 [Wiltamuth 1997a],其中包括:

  • 初始标准的范畴,将不涉及特定于宿主的库对象与函数。例如那些应由浏览器和 Web 服务端宿主提供的规范。
  • 只有在完整的规范草案可用后,才考虑对当前语言的扩展。
  • 逗号和 ? 运算符不会传播(propagate)引用值,因此它们既不能在赋值运算的左侧使用,也不能作为函数调用的 this 值。
  • 标识符中不允许使用非 ASCII Unicode 字符。
  • 字符串值支持使用 NUL(U+0000)字符。
  • 全局函数和变量声明会创建可枚举、可删除的属性,而规范中定义的内置对象属性则默认为不可枚举但可删除的。

在第一次工作组会议上未解决的问题包括:多次赋值的求值顺序、对继承的只读属性赋值时的语义,以及如何适应 1970 年之前的日期值。

工作组(图 15)在 1997 年 4 月中旬之前定期开会,研究了一系列的主要和次要问题,并审查了编辑编写的工作草案文本。有九次工作会议留下了记录 [Wiltamuth 1997a, b, c, d, e, f, g, h, i]。参加了一些工作组会议的 Richard Gabriel 在个人交流中回忆说,这些会议期间的互动并不罕见。Guy Steele 会询问一些边界条件下特性行为的问题。有时 Brendan Eich 会说「我不知道」,有时 Eich 和 Shon Katzenberger 可能不太确定或产生分歧。在这种时候,他们会在各自的实现中尝试测试用例。如果得到相同的答案,这个答案就会成为被确定下来的行为;如果出现差异,他们将会就问题讨论到达成共识为止。

Scott Wiltamuth (note taker)Microsoft
Brendan EichNetscape
Shon KatzenbergerMicrosoft
Michael Gardner (1st draft co-editor)Borland
Randy Solton (1st draft co-editor)Borland
Clayton LewisNetscape
Guy Steele (editor)Sun

图 15. ES1 规范工作组的定期参与者。

在第一份 Gardner 和 Solton 起草的规范草案之后,Guy Steele 在 1997 年 2 月 27 日至 5 月 2 日之间,向整个委员会发布了另外七份草案,其余的工作草案则在工作组内分发。除了 Ecma GA 大会的最终草案 [TC39 1997b] 之外,每份草案都包含详细的问题解决日志 [TC39 1997d]。

规范制定过程中的某些问题,对语言的使用产生了长期的影响。比如有个受到持续讨论的问题是这样的:短路布尔运算符 &&|| 在遇到可转换为布尔值的操作数时,是应该求值为其中一个操作数的实际值(所谓「Perl 风格」),还是 truefalse 的布尔值(所谓「Java 风格」)。Brendan Eich 最初的实现主要使用了「Perl 风格」的语义,但少数情况下也有「Java 风格」的行为。微软和 Borland 则已经实现了完整的「Java 风格」语义。最终决定是一致采用「Perl 风格」。

这个决定直接促成了几年后广泛使用的 JavaScript 惯用法。布尔运算符将 nullundefined 的值转换为 false,并将所有的对象引用转换为 true。这就带来了如图 16 所示的手法,它为对象属性和可选的函数参数提供了默认值。

  1. function f(options) {
  2. options = options || getDefaultOptionsObject();
  3. // 如果传递了 options 对象,那么就使用它
  4. // 否则使用默认的一组配置
  5. // ...
  6. }

图 16. ECMAScript 1 中为函数形参赋予默认值的手法。

Brendan Eich 回忆说,他希望加入 JavaScript 1.2 中自己对 == 运算符语义的更改,以消除其类型转换问题。Shon Katzenberger 成功地说服了他,理由是鉴于会破坏大量现有 Web 页面,现在做这种更改已经为时已晚。Eich 在 JavaScript 1.3 的 SpiderMonkey 版本中恢复了原始的语义。

TC39 的第三次会议是 1997 年 3 月 18 日至 19 日举行的。这是 6 月 Ecma GA 大会前最后一次排定的 TC39 正式会议,目标是让标准的第一版能获得接受和批准。为了满足这份时间表,TC39 需要在这次会议上投票,以将标准提交给 GA 大会。

在 3 月 12 日,标准的 0.12 版本草案 [TC39 1997a] 分发给了全体委员会,并在 3 月 14 日的工作组会议上进行了讨论 [TC39 1997f]。这份草案在技术上已经接近完成,只是 Date 对象的复杂定义仍然只是简单的一组标题。Shon Katzenberger 提出了关于规范质量的完整提案。经过讨论和审查,这份提案也可以被纳入规范。从 1 月 10 日草案完成的两个月以来,这份文档包含的实质性页面已从 41 页增加到了 96 页。0.12 版草案中除了缺少 Date 规范外,其问题跟踪附录中还有 8 个内部「问题」标签和 6 个重要条目。工作组会议还讨论了大约 12 个需要在规范中解决的其他问题。

由于 Scott Wiltamuth 保证所有问题都不会遗留下争议,并且完整的草案可以在 3 月底完成,因此 TC39 一致同意将草案交给 Ecma GA 大会,以进行 6 月的赞成投票。工作组被赋予的职责是收尾规范,并与 Ecma 秘书处的工作人员一起制定出符合其时间表和格式要求的最终草案。草案的完成比 Wiltamuth 的估计多花了一个月的时间。在 1997 年 5 月 2 日完成最终草案 [TC39 1997b] 前,工作组内部又分发了三份中间草案。最终草案于 5 月 5 日分发给了 GA 大会成员。最终草案符合 Ecma 的文档约定,并包含了 Richard Gabriel 对语言的非规范性g概述。GA 大会在 1997 年 6 月的会议上同意在稍作编辑更改后,将草案发布为《Ecma 标准 ECMA-262 第 1 版》,并将其提交到了 ISO 快速通道流程中。编辑更改完成后,草案于 1997 年 9 月 10 日分发给了 TC39。《ECMA-262 第 1 版》[Steele 1997] 在 9 月 16 日至 17 日的 TC39 会议上 [1997h] 正式发布。