编写规范
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 Conventions | Notational Conventions |
Source Text | Source Text |
Lexical Conventions | Lexical Conventions |
Types | Types |
Type Conversion | Type Conversion |
Variables | Execution Contexts |
Expressions | Expressions |
Statements | Statements |
Function Definition | Function Definition |
Program | Program |
Native ECMAScript Objects | Native ECMAScript Objects |
Errors |
图 13. ECMAScript 标准的结构。
草案中对语法约定的描述,主要来自 Netscape 的规范。但至于表达式与语句级语法的结构,以及产生式(production)的名称,则在很大程度上遵循了微软规范中的用法。在两份贡献出的规范中,表达式语法在细微的细节层面上有所不同,例如函数调用的优先级、对象的创建(new
运算符),以及对象属性访问表达式的元素等。
这份草案试图将自动分号插入(ASI)的规则,精确地定义为用来「校正解析错误」的过程。语句的语法包括了显式的分号,用于终止所有非复合语句。如果没有 ASI,那么缺失分号将会产生解析错误。ASI 规范定义了 JavaScript 解析器何时必须通过「假设存在分号」并重新解析的方式,来尝试纠正此类解析错误。第一版草案中的 ASI 规则并不完整,这在后来的 ECMAScript 规范草案和发行版中进行了完善。
1 月 10 日的草案中包含了 Shon Katzenberger 的伪代码算法(例如图 14),用于定义各种语言结构的语义。具体算法的组成,则包括了带顺序编号的步骤,以及步骤之间的简单条件控制流。每个步骤都包含一些命令式g的叙述。步骤的叙述用英语编写,并结合了规范中针对常见动作所定义的基本词汇。可以在规范内的其他算法中命名和「调用」这些算法。
4.4.7 GetValue(V)
1. If Type(V) is not a Reference, return V.
2. Call GetBase(V)
3. If Result(2) is null, generate a runtime error.
4. Call the [[Get]] method of Result(2), passing GetProperty(V) for the property name and GetAccess(V) for the access mode.
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 Eich | Netscape |
Shon Katzenberger | Microsoft |
Michael Gardner (1st draft co-editor) | Borland |
Randy Solton (1st draft co-editor) | Borland |
Clayton Lewis | Netscape |
Guy Steele (editor) | Sun |
图 15. ES1 规范工作组的定期参与者。
在第一份 Gardner 和 Solton 起草的规范草案之后,Guy Steele 在 1997 年 2 月 27 日至 5 月 2 日之间,向整个委员会发布了另外七份草案,其余的工作草案则在工作组内分发。除了 Ecma GA 大会的最终草案 [TC39 1997b] 之外,每份草案都包含详细的问题解决日志 [TC39 1997d]。
规范制定过程中的某些问题,对语言的使用产生了长期的影响。比如有个受到持续讨论的问题是这样的:短路布尔运算符 &&
和 ||
在遇到可转换为布尔值的操作数时,是应该求值为其中一个操作数的实际值(所谓「Perl 风格」),还是 true
或 false
的布尔值(所谓「Java 风格」)。Brendan Eich 最初的实现主要使用了「Perl 风格」的语义,但少数情况下也有「Java 风格」的行为。微软和 Borland 则已经实现了完整的「Java 风格」语义。最终决定是一致采用「Perl 风格」。
这个决定直接促成了几年后广泛使用的 JavaScript 惯用法。布尔运算符将 null
和 undefined
的值转换为 false
,并将所有的对象引用转换为 true
。这就带来了如图 16 所示的手法,它为对象属性和可选的函数参数提供了默认值。
function f(options) {
options = options || getDefaultOptionsObject();
// 如果传递了 options 对象,那么就使用它
// 否则使用默认的一组配置
// ...
}
图 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] 正式发布。