定义 ECMAScript 3

在第一次 TC39 会议上,涌现出了许多对 JavaScript 1.0/1.1 语言的扩展,其中一些扩展也合并到了语言规范的初稿中。但是 TC39 技术工作组同意优先完成基本语言规范,而后才考虑新特性。因此对第一版来说,大部分可能的扩展都归入了规范草案的附录中 [TC39 1997a, Appendix B]。

到 1997 年 7 月的 TC39 会议 [1997g] 时,第一版的工作已接近完成。委员会考虑的重点转移到了下一版规范中所应包含的新特性。Netscape 已经表明了其 Netscape 4.0 的发展方向,其中会将 SpiderMonkey 引擎与 JavaScript 1.2 的扩展相结合。Scott Wiltamuth 则提出了微软 [1997] 关于「ECMAScript 2.0」的初步提案,其中包括 switch 语句、do while 语句,以及带有标签的 breakcontinue 语句。一并包含的还有 ===!== 运算符,以及将 caller 属性添加到 arguments 对象。微软的 Andrew Clinick [1997] 提出了一份单独的提案,希望增加条件编译支持。微软在 10 月将 JScript 3.0 作为 Internet Explorer 4.0 的组件发布时,确定了「第二版」的起点。图 17 列出了截至 1997 年底,由 Netscape [1997c] 和微软 [2009b] 浏览器为 ECMAScript 第一版实现的主要扩展。

特性JavaScript 1.2JScript 3.0ECMA-262 第 3 版
do 语句
break/continue 到标签
switch 语句
嵌套函数
函数表达式
对象字面量
数组字面量
===!==
正则表达式字面量
delete 运算符
所有对象上的 proto 伪属性
数组方法 concat, slice
数组方法 push, pop, shift, splice, unshift
带有继承元素的稀疏数组
使用正则表达式的字符串方法 fromCharCode, match, replace, search, substr, split
字符串方法 charCodeAt
正则表达式方法 compile, exec, test
正则表达式属性 $1$9, input
正则表达式全局属性 lastMatch, lastParen, leftContext, rightContext
带有本地声明属性的 arguments 对象
arguments.callee
arguments.caller
watch/unwatch 函数
import/export 语句与脚本签名
条件编译
debugger 关键字

图 17. 主流浏览器在 1997 年对 ECMA-262 第一版的扩展。它们中的多数最终包含在了 ECMA-262 第三版中。

TC39 的正式会议已经改由代表成员公司的小组与项目经理参加,转为了管理和战略会议。而整个委员会的大部分技术工作,都发生在非正式技术工作组中。在 7 月的会议上,TC39 商定了开发第二版的一系列步骤。委员会还达成了共识,认为技术工作组有责任定义工作项目、特性提案和验收标准。第二版分配到的时间要比第一版更多,以使草案进一步成熟并获得外部反馈。第二版规范初稿的目标日期是 1997 年 12 月。在 9 月的会议上 [TC39 1997h],人们还同意第二版规范必须向后兼容那些符合第一版规范的程序。

在做出这些决定时,ISO 快速通道流程尚未开始。这时还没有人知道,由此产生的更改将需要发布新版 ECMA-262 标准,才能与 ISO 版本保持一致。在 1998 年初,一度有两个成员互相重叠的工作组,分别负责两份单独的规范草案。显然,这里的「第二版」(提交给 ISO 的 Edition 2)和「第二版」(包含新特性的 Version 2)已经不大可能合并。但是尽管 TC39 代表们已经知道这个版本可能会发布为「第三版」,他们还是继续将下一轮功能性工作叫做「第二版」或「V2」。像这样 TC39 的内部版本命名与最终的发布术语相冲突的情况,后面还会发生。

到 1997 年底,技术工作组的参与者发生了重大变化。图 18 列出了 1998 年间在工作组会议记录中出现的个人。在开发第一版的工作组常规参与者中,只有 Clayton Lewis 仍然保持活跃。Brendan Eich 在 1998 年 2 月参加了一次会议,而后成为了 Mozilla 项目 [Mozilla Organization 1998] 的联合创始人,致力于开源 Netscape 浏览器的代码,由 Waldemar Horwat 接任 TC39 的 Netscape 语言设计负责人。无独有偶,微软的 Katzenberger 也在休假后转入其他项目,Herman Venter 和 Rok Yu 接替了他代表微软在 TC39 的职责。

Norris BoydNetscapeDrew LytleMicrosoft
Andrew ClinickMicrosoftKarl MatzkeSunSoft
Mike CowlishawIBMMick McCabeNetscape
Jeff DyerNombasDave RaggetHP/W3C
Bill GibbonsNetscapeHerman VenterMicrosoft
Waldemar HorwatNetscapeRok YuMicrosoft
Mike KsarHPChris WeightMicrosoft
Clayton LewisNetscape

图 18. 1998 年 TC39 技术工作组的经常性参与者。

在 1997 年 10 月,技术工作组为能包含在第二版中的特性列出了清单(Appendix H)。在这里获得认可而列出的特性中,除了一些例外,主要都是 Netscape JavaScript 1.2 和微软 JScript 3.0 特性的结合。还有 toSource 也包括在内,对应于 Brendan Eich 为 JavaScript 1.3 开发的对象序列化与持久性方案37。其他已在设想中但缺乏共识的特性则另外列出。与第一版一样,工作组的大部分注意力都集中在精确指定已实现的特性,并解决实现之间存在的差异。但是,商定的特性列表里还包括异常处理机制、instanceof 运算符,以及尚未实现的所有其他特性。开发这些特性将需要某种设计工作,这在第一版中是不必的。图 19 列出了一些 1998 年前的浏览器所没有的特性,这些特性最终都包含在了 ES3 中。

  1. * try-catch-finally 和异常对象
  2. * instancesof in 运算符
  3. * 对象原型方法: hasInstance, hasOwnProperty, isPrototypeOf, propertyIsEnumerable
  4. * undefined 的全局绑定
  5. * toFixed, toExponential, toPrecision
  6. * URI 处理器函数
  7. * 标识符中的 Unicode 字符
  8. * 基础的 I18N 方法: Object toLocaleString; Array toLocaleString; Number toLocaleString; String localeCompare, toLocaleLowerCase, toLocaleUpperCase; Date toLocaleDateString, toLocaleTimeString

图 19. 1998 年前的浏览器所缺乏的 ES3 新特性。它们中的一些在 TC39 开发 ES3 时就集成到了浏览器里。

技术工作组按每月面对面开会的节奏设定了规划。Mike Cowlishaw [1999b; Appendix I] 维护了一份文档,以跟踪规范各部分的当前状态。状态指示器如下:「自 V1 起未更改」、「尚未准备就绪」、「需要讨论」,「特性已接受」和「内容已达成共识」。状态「特性已接受」表示委员会对规范中定义的功能性表示同意,状态「内容已同意」则表示实际的规范文本已经过审核而被接受。

Bill Gibbons 是新规范工作草案的编辑。每次会议都有一个介绍和讨论各种提案和未解决问题的议程。提案被提出的形式,则通常是提交新的或修订后的算法规范文本。会议还进行了一般状态审核,由与会人员讨论自上次会议以来确定的问题。当就提案或问题解决方案达成协议时,Gibbons 会将其纳入工作草案。V2 版本的第一份完整草案 [Cowlishaw et al. 1998] 发布于 1998 年 4 月,基于 ECMA-262 第一版,其中没有包含任何为 ECMA-262 第二版(ISO 版本)同时开发的更改。工作草案的标题页指出,这里包含的是 Netscape 和微软提交的拟议更改。在 9 月 ISO 版本完成后,Gibbons 将 ES2 更改合并到了当前的 V2 工作草案中。

当时 Unicode 仍然是一种新技术,语言设计人员还在探索将其集成到编程语言中的最佳实践。有个需要特别关注的问题,即如何处理 Unicode 的各种正规化(normalization)形式,这些形式允许对行为等效的字符序列进行替代编码。ES1 对 Unicode 的支持很少。惠普的 Tom McFarland 参加 1998 年 5 月的会议后提交了一份备忘录 [McFarland 1998],指出了他认为与国际化g(I18N)有关的许多问题,以及如何将 Unicode 更好地集成到 ECMAScript 中。经过几次会议的讨论,TC39 在 1998 年 11 月建立了一个由 IBM 的 Richard Gillam [1998] 主持的「I18N 工作组」。I18N 小组很快决定将重点放在针对核心语言的少量基本 I18N 特性上 [Gillam et al. 1999b],并将关于国际化和本地化更复杂的内容推迟,将它们纳入单独定义的可选库中 [Gillam et al. 1999a, b]。但直到 2012 年,这些类库的规范 [Lindenberg 2012] 才得以完成。除了为核心语言添加了少量区域特定(locale-specific)特性外,I18N 小组还解决了如何将非拉丁字符合并到标识符中的问题。它推荐 ECMAScript 规范对于提供给实现的源代码,可以假定其均采用 Unicode 的正规形式 C(Normal Form C)来编写。这在很大程度上避免了正规化问题。它还选择不对核心语言中的 Unicode 正规化提供任何支持,并把对正规化的编程支持推迟纳入可选库中。

V2 的主要任务,是为语言设计异常处理机制。1998 年 2 月 [TC39 1998c],微软的 Herman Venter 和 Netscape 的 Waldemar Horwat 均提出了设计草案。两种设计都多少参考了 Java 的 try-catch-finally 语句语法,但它们和 Java 在语法和语义上都存在着显著的差异。

在微软的设计 [Venter 1998b] 中,任何值都可以作为异常抛出,并且 try 语句具有单个 catch 子句,它声明了一个初始化为「被捕获的异常值」的局部变量。从 try 块传播的所有异常都会被无条件捕获,没有 finally

Netscape 的设计 [Horwat 1998] 还允许将任何值作为异常抛出。但在这种设计中,try 语句可能具有多个 catch 子句38,其中带有将 instanceof 用作鉴别符(discriminator)的语法,以确定要执行哪个 catch 的子句。如果没有 catch 子句与异常匹配,那么在执行 finally 子句后,还会继续在调用栈中传播异常。instanceof 鉴别符最终被 if 鉴别符39所取代,它会将表达式求值为布尔值,以确定是否选中了想要的 catch

在 1998 年 2 月的会议上 [TC39 1998d],委员会同意使用 trycatch 关键字,并且 throw 语句可以传播任何值(不仅是特定内置异常类的实例)来表示异常。在 1998 年 3 月的工作组会议上,Waldemar Horwat 主张加入 finally 子句,并同意进一步研究相应实现的细节。4 月的工作草案 [Cowlishaw et al. 1998] 合并了 Netscape 的设计,但当时尚未解决的问题包括:对 finally 的支持、catch 变量绑定的作用域、是否允许多个 catch 子句、是否应该将 instanceof 用作 catch 的选择器,以及是否应自动重新抛出未被选中的异常。图 20 提供了一些示例,展示了微软的提案、Netscape 修改后的提案,以及最终在 ES3 中确定的语法。注意 Netscape 的设计使用了单独的选择器表达式来选择 catch 子句。但在微软和最终的 ES3 设计中,则需要使用单个 catch 块中的用户逻辑来区分不同的异常。

  1. // 微软的设计
  2. try {
  3. doSomething();
  4. } catch (var e) {
  5. if (e == "thing")
  6. console.log("a thing")
  7. else if (e == 42)
  8. console.log("42")
  9. else {
  10. console.log(e);
  11. cleanup();
  12. throw e; // 重新 throw
  13. }
  14. // 没有 finally 语法
  15. }
  16. cleanup();
  17. // Netscape 的设计
  18. try {
  19. doSomething();
  20. } catch (e if e == "thing") {
  21. console.log("a thing")
  22. } catch (e2 if e2 == 42) {
  23. console.log("42")
  24. } catch (e3) {
  25. console.log(e3);
  26. throw e3; // 重新 throw
  27. } finally {
  28. cleanup();
  29. }
  30. // 第 3 版规范的最终设计
  31. try {
  32. doSomething();
  33. } catch (e) {
  34. if (e == "thing")
  35. console.log("a thing")
  36. else if (e == 42)
  37. console.log("42")
  38. else {
  39. console.log(e);
  40. throw e; // 重新 throw
  41. }
  42. } finally {
  43. cleanup();
  44. }

图 20. 异常处理的几种设计。在这些示例中,doSomething 函数可能抛出两种异常,它们在当前函数继续执行前都需要单独处理。所有其他异常都被「重新抛出」以传播给当前函数的 caller。当前函数还具备 cleanup 流程,不管 doSomething 是否抛出异常都会执行。

直到 1999 年 9 月对标准草案进行最终技术审查 [TC39 1999b] 前,语言是否应支持多个 catch 子句的问题一直没有得到解决。这个特性最终推迟留待未来考虑。同样在最后的审查中,委员会才就标准将定义的内置异常类达成了共识。

在将 Java 和其他静态类型g基于类的语言中的特性,适配到使用动态类型和原型继承的 JavaScript 时,委员会遇到了一些困难。像 catch 子句的守卫表达式(guard expression)就是这其中的一个例子。在 Java 中,要由哪个 catch 子句处理抛出的异常,是通过无副作用的「子类型包含测试」来确定的。这种测试完全依赖静态声明的类层次结构,可以在实际恢复调用栈现场(call stack unwinding)之前执行。但是 JavaScript 则既没有正式的类概念,也没有静态的类层次结构。由于委员会已经决定支持抛出任何类型的值作为异常,故而要想在 JavaScript 的 catch 子句中区分出任意的值,就需要求值任意的守卫表达式,这其中可能包含赋值和函数调用。但是,对表达式的求值需要建立适当的词法和动态环境,并且每次对守卫表达式的求值都可能产生副作用,这些副作用可能会改变后续守卫表达式的求值结果。在一份中立提案中,Waldemar Horwat [1998] 提出了一种复杂的叙述性规范,它允许实现者决定「何时」以及「以何种顺序」来对 catch 到的守卫表达式求值,甚至还允许多次对单个守卫表达式求值。Horwat 希望使调试器在恢复现场前,能够确定是否还有「未被处理的抛出异常」。幸好这个设计未被接受,因为随后的经验表明,这种实现方式上的差异,是网页在兼容多个浏览器时互操作性问题的重要来源。

另一个 TC39 难以将语言的概念和构造从 Java 转换为 JavaScript 的例子,则是 instanceof 运算符。在 Java 中,instanceof 是一个二元运算符,用于测试其左操作数的对象是否为右操作数的「类实例」或「子类实例」。Herman Venter [1998a] 最初提出的 instanceof 提案限制了右操作数仅限标识符,这样就完全模仿了 Java 的语法。但是 JavaScript 本质上没有类的概念,并且还有多种创建新对象的方法。Venter 的提案假定使用构造函数模式作为测试 instanceof 的基础。这样一来,右操作数就可以动态地求值到构造函数对象,而这是个一等的函数值。由于这样的右操作数是一等的值而非类型引用,因此提案不久就泛化支持了在该位置上出现表达式。instanceof 的运行时语义被定义为:遍历左操作数的原型继承链,搜索值为右操作数 prototype 属性当前值的对象。对于许多简单的构造函数,这将会匹配到那些将 new 运算符应用到它们上面而创建的对象。

具备 Java 背景的新 JavaScript 程序员会认为 instanceof 是区分各种对象的可靠方法,但许多经验丰富的 JavaScript 程序员会都避免使用它。这是因为构造函数返回的对象未必能通过动态的 instanceof 测试,并且由于对象元结构的可变性,对 instanceof 的重复应用可能不是幂等的。如果要测试的对象来自与构造函数不同的 HTML 页框,测试也可能失败。最后,即使结果为真,被测试的对象仍然可能没有由构造函数创建的数据和行为属性。

ES3 包含了内部函数声明和函数表达式,它们与 JavaScript 1.2 中最初引入的概念相似。函数声明被明确排除在 {} 语句块之外,也不能作为子语句使用。Waldemar Horwat [2008b] 后来解释了原因:

  1. 将这类声明提升到最高层级(像 var 那样)的做法是无效的。因为在这样的函数能捕获的作用域里,可以包含尚不存在的变量。ES3 没有局部作用域,但确实有会导致相同问题的异常作用域。当我们考虑将语言扩展为支持常量和动态(即运行时)类型注释后的场景时,情况还会变得更糟——这样的函数可以捕获尚未创建的常量,甚至还可以捕获尚未计算出类型的变量!

  2. 可以选择等到遇到此类声明时再绑定它们,这样也确实可行。但我们不想仅出于对函数的支持,就在 ES3 中实现这样的本地绑定。

  3. 在这类声明位于 if 语句的子语句位置时,规划中的设想是仅在 if 表达式为真(对 else 子句为假)时创建这些声明,并将其放入最接近的封闭块级作用域内。这就构成了某种形式的条件编译。而一个语句块如果前面有标记(attribute),那它就是一个非作用域块,这个块会把标记分配给它所包含的定义。于是这样就可能把多个定义附加到一条 if 语句了。

主要的浏览器都忽略了这些意见,选择继续在块内实现函数声明。然而,每种实现都为这些声明发明了自己的独特语义。十五年后,这为 ES6 [TC39 2013b, Function In Block Options; §21.3.2] 的设计者带来了重大的问题。

到 1999 年春季,第三版规范明显还无法在 6 月的 GA 大会上获得批准,但等到 12 月可能还有机会。在 3 月,工作组进行了分类 [Clinick 1999],以识别出那些为达成 12 月目标而需要砍掉或推迟的特性。被永久性移除的特性包括:__proto__ 属性、# 变量、用于堆栈实化(stack reification)的调用对象(call object),以及显式的闭包对象。推迟到可能在未来版本中加入的特性则包括:原子操作、异常 catch 的守卫、条件编译、日期标量、十进制小数运算、泛型序列运算符、可选的 I18N 库、外部函数接口(FFI)、基于 toSource 的对象持久化、对数值单位的语法和运算支持,以及可扩展的字面量语法。

工作组在 1999 年 5 月至 1999 年 9 月间举行了四次会议,以解决有关第三版规范最终草案的问题。在此期间必须解决的重大设计问题包括:正则表达式匹配语义算法规范的创建、一组内置异常类型的确定、函数表达式绑定语义的确定,以及将 Unicode 支持合并到语言中时的细节。

1999 年 8 月 8 日,Mike Cowlishaw [1999c] 发布了最终的「E3 草案状态」,展示了所有状态为「内容已同意」或「自 V1 以来未更改」的章节。8 月 25 日,Bill Gibbons [1999] 发布了「第三版 3 最终草案」,并离开委员会开始了新工作。Herman Venter 和 Waldemar Horwat 负责将所有剩余的更改纳入草案。

在最后的 ES3 开发会议 [TC39 1999b] 中,Horwat 准备了很长的笔记清单,以标识对次要编辑和技术问题的更正,这其中只有少数变化会影响 JavaScript 程序员的日常。内置异常 ConversionErrorRegExpError 被移除,由 TypeErrorSyntaxError 取代。

对于 FunctionExpression40(函数表达式)中允许在函数名称位置出现的可选标识符,8 月的草案没有为其指定任何含义。例如:

  1. function fact(n) { throw "wrong fact" }; // 函数声明
  2. var lambdaFact = function fact(n) { // 这个函数表达式,是否应该绑定到 fact 上?
  3. return n <= 1 ? 1 : fact(n - 1);
  4. };
  5. lambdaFact(5); // 应该递归还是抛出异常?

在这份草案中,调用 lambdaFact 会抛出异常。这是因为这里 FunctionExpression 起始位置的 fact 名称,并没有为 fact 创建词法绑定。在 9 月的会议上达成了对规范的修订意见,会为这个名称创建一个到相应函数的本地名称绑定,这个绑定只在 FunctionExpression 的语句体内可见。

在最后时刻还有个最令人惊讶的新增特性,即 Waldemar Horwat 在会议上提出的「函数合并」(joined functions)。只要实现支持该特性,就可以在如下情况时重复返回相同的函数闭包对象:

  1. function getClosure() { return function () {/* 没有对自由变量的引用 */ } }
  2. var firstTime = getClosure();
  3. var secondTime = getClosure();
  4. // 下面的比较是 true 还是 false 由实现决定
  5. console.log(firstTime === secondTime); // 是否是相同对象?

Waldemar Horwat 担心闭包创建的开销,并认为这个改动将可以让实现在某些常见情况下复用闭包。Herman Venter 表示了一些担忧,但在会议结束时同意支持这个改动。这本可能造成一个重大的设计错误,因为随后 Web 浏览器上的经验表明,这种特性所允许的某种在实现间可见的差异,可能会妨碍网站在不同浏览器上的正常工作。幸运的是,并没有浏览器实现函数合并特性,它在 2009 年也从 ES5 规范中删除。

由于在字符串字面量中,对八进制常量(以 0 开头的数字写法)和八进制转义序列的使用不被提倡,它们从 规范的g标准中移到了非规范性的附录 B41(Annex B)中。一并移至附录 B 的内容包括:与 Y2K 不兼容的 Date 方法、escapeunescape 字符串函数,以及字符串方法 substr。这些特性都已被认定为过时,但仍被网站使用。此举背后的设想,在于特性一旦在标准的非规范性附录 B 中列出,即表明它们已被废弃而不应继续使用,各实现均有权最终删除它们。这是个幼稚的期望。TC39 成员尚未意识到,浏览器实现者们非常不愿意删除网页上实际可能用到的任何特性(不论是否标准化)——某些网页永远不会消失。

在审查并解决了所有未解决的问题后,TC39 一致接受规范,认为它已经完备,遵从并纳入了会议中所提出的更改要求。Waldemar Horwat 和 Herman Venter 准备了最终文档 [TC39 1999e],并于 1999 年 10 月 13 日将其交给了 Ecma 秘书处。最终草案中有一张表,其中列出了 ECMA-262 前三个版本的所有贡献者(图 21),包括内容创作、技术会议参与,以及通过电子邮件的贡献。

Mike AngGary FisherClayton LewisSam Ruby
Christine BegleRichard GabrielDrew LytleDario Russi
Norris BoydMichael GardnerBob MathisDavid Singer
Carl CargillBill GibbonsKarl MatzkeRandy Solton
Andrew ClinickRichard GillamMike McCabeGuy Steele
Donna ConverseWaldemar HorwatTom McFarlandMichael Turyn
Mike CowlishawShon KatzenbergAnh NguyenHerman Venter
Chris DollinCedric KrumbeinBrent NoordaGeorge Wilingmyre
Jeff DyerMike KsarAndy PalayScott Wiltamuth
Brendan EichRoger LawrenceDave RaggettRok Yu
Chris EspinosaSteve LeachGary Robinson

图 21. ECMA-262 第 1、2、3 版的技术贡献者。

在 11 月,最终草案中有一些较小的编辑和技术错误被确定并更正 [TC39 1999a]。其中最值得注意之处,在于微软发现当为了符合最终草案,用正则表达式来改动 JScript 的 String.replace 实现时,许多网站(包括 microsoft.com 在内)会出现问题。TC39 同意更改规范,从而与微软之前的实现相匹配。

1999 年 12 月 16 日,Ecma GA 大会 [Ecma International 1999] 批准了该规范,是为《ECMA-262 第 3 版》[Cowlishaw 1999a]。自 2000 年 3 月起,Waldemar Horwat [2003b] 维护了一份非正式的 ES3 勘误表。主流浏览器陆续在 2000 年发布了与 ES3 兼容的版本。微软的 JScript 5.5 作为 IE 5.5 的一部分于 2000 年 7 月发布,而 Netscape 的 JavaScript 1.5 则作为 Netscape 6 的一部分于 2000 年 11 月发布。直到 2009 年 12 月为止,《ECMA-262 第 3 版》都没有被更新的版本替代。在此期间,浏览器并不能自动更新,并且许多用户只有在拥有新计算机或新版操作系统时,才会更新浏览器。等到 Web 开发者可以假设所有用户都使用支持 ES3 的浏览器时,已经过去了将近十年。

插曲:JavaScript 不需要 Java

最初,JavaScript 被认为是 Java 的辅助脚本语言,所有复杂的编程任务都将使用 Java 来完成。但是随着对 JavaScript 的熟悉,Web 开发者们开始意识到他们其实只要有 JavaScript 就够了。