定义 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
语句,以及带有标签的 break
和 continue
语句。一并包含的还有 ===
和 !==
运算符,以及将 caller
属性添加到 arguments
对象。微软的 Andrew Clinick [1997] 提出了一份单独的提案,希望增加条件编译支持。微软在 10 月将 JScript 3.0 作为 Internet Explorer 4.0 的组件发布时,确定了「第二版」的起点。图 17 列出了截至 1997 年底,由 Netscape [1997c] 和微软 [2009b] 浏览器为 ECMAScript 第一版实现的主要扩展。
特性 | JavaScript 1.2 | JScript 3.0 | ECMA-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 Boyd | Netscape | Drew Lytle | Microsoft |
Andrew Clinick | Microsoft | Karl Matzke | SunSoft |
Mike Cowlishaw | IBM | Mick McCabe | Netscape |
Jeff Dyer | Nombas | Dave Ragget | HP/W3C |
Bill Gibbons | Netscape | Herman Venter | Microsoft |
Waldemar Horwat | Netscape | Rok Yu | Microsoft |
Mike Ksar | HP | Chris Weight | Microsoft |
Clayton Lewis | Netscape |
图 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 中。
* try-catch-finally 和异常对象
* instancesof 和 in 运算符
* 对象原型方法: hasInstance, hasOwnProperty, isPrototypeOf, propertyIsEnumerable
* undefined 的全局绑定
* toFixed, toExponential, toPrecision
* URI 处理器函数
* 标识符中的 Unicode 字符
* 基础的 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],委员会同意使用 try
和 catch
关键字,并且 throw
语句可以传播任何值(不仅是特定内置异常类的实例)来表示异常。在 1998 年 3 月的工作组会议上,Waldemar Horwat 主张加入 finally
子句,并同意进一步研究相应实现的细节。4 月的工作草案 [Cowlishaw et al. 1998] 合并了 Netscape 的设计,但当时尚未解决的问题包括:对 finally
的支持、catch
变量绑定的作用域、是否允许多个 catch
子句、是否应该将 instanceof
用作 catch
的选择器,以及是否应自动重新抛出未被选中的异常。图 20 提供了一些示例,展示了微软的提案、Netscape 修改后的提案,以及最终在 ES3 中确定的语法。注意 Netscape 的设计使用了单独的选择器表达式来选择 catch
子句。但在微软和最终的 ES3 设计中,则需要使用单个 catch
块中的用户逻辑来区分不同的异常。
// 微软的设计
try {
doSomething();
} catch (var e) {
if (e == "thing")
console.log("a thing")
else if (e == 42)
console.log("42")
else {
console.log(e);
cleanup();
throw e; // 重新 throw
}
// 没有 finally 语法
}
cleanup();
// Netscape 的设计
try {
doSomething();
} catch (e if e == "thing") {
console.log("a thing")
} catch (e2 if e2 == 42) {
console.log("42")
} catch (e3) {
console.log(e3);
throw e3; // 重新 throw
} finally {
cleanup();
}
// 第 3 版规范的最终设计
try {
doSomething();
} catch (e) {
if (e == "thing")
console.log("a thing")
else if (e == 42)
console.log("42")
else {
console.log(e);
throw e; // 重新 throw
}
} finally {
cleanup();
}
图 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] 后来解释了原因:
将这类声明提升到最高层级(像
var
那样)的做法是无效的。因为在这样的函数能捕获的作用域里,可以包含尚不存在的变量。ES3 没有局部作用域,但确实有会导致相同问题的异常作用域。当我们考虑将语言扩展为支持常量和动态(即运行时)类型注释后的场景时,情况还会变得更糟——这样的函数可以捕获尚未创建的常量,甚至还可以捕获尚未计算出类型的变量!可以选择等到遇到此类声明时再绑定它们,这样也确实可行。但我们不想仅出于对函数的支持,就在 ES3 中实现这样的本地绑定。
在这类声明位于
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 程序员的日常。内置异常 ConversionError
和 RegExpError
被移除,由 TypeError
和 SyntaxError
取代。
对于 FunctionExpression40(函数表达式)中允许在函数名称位置出现的可选标识符,8 月的草案没有为其指定任何含义。例如:
function fact(n) { throw "wrong fact" }; // 函数声明
var lambdaFact = function fact(n) { // 这个函数表达式,是否应该绑定到 fact 上?
return n <= 1 ? 1 : fact(n - 1);
};
lambdaFact(5); // 应该递归还是抛出异常?
在这份草案中,调用 lambdaFact
会抛出异常。这是因为这里 FunctionExpression 起始位置的 fact
名称,并没有为 fact
创建词法绑定。在 9 月的会议上达成了对规范的修订意见,会为这个名称创建一个到相应函数的本地名称绑定,这个绑定只在 FunctionExpression 的语句体内可见。
在最后时刻还有个最令人惊讶的新增特性,即 Waldemar Horwat 在会议上提出的「函数合并」(joined functions)。只要实现支持该特性,就可以在如下情况时重复返回相同的函数闭包对象:
function getClosure() { return function () {/* 没有对自由变量的引用 */ } }
var firstTime = getClosure();
var secondTime = getClosure();
// 下面的比较是 true 还是 false 由实现决定
console.log(firstTime === secondTime); // 是否是相同对象?
Waldemar Horwat 担心闭包创建的开销,并认为这个改动将可以让实现在某些常见情况下复用闭包。Herman Venter 表示了一些担忧,但在会议结束时同意支持这个改动。这本可能造成一个重大的设计错误,因为随后 Web 浏览器上的经验表明,这种特性所允许的某种在实现间可见的差异,可能会妨碍网站在不同浏览器上的正常工作。幸运的是,并没有浏览器实现函数合并特性,它在 2009 年也从 ES5 规范中删除。
由于在字符串字面量中,对八进制常量(以 0
开头的数字写法)和八进制转义序列的使用不被提倡,它们从 规范的g标准中移到了非规范性的附录 B41(Annex B)中。一并移至附录 B 的内容包括:与 Y2K 不兼容的 Date 方法、escape
和 unescape
字符串函数,以及字符串方法 substr
。这些特性都已被认定为过时,但仍被网站使用。此举背后的设想,在于特性一旦在标准的非规范性附录 B 中列出,即表明它们已被废弃而不应继续使用,各实现均有权最终删除它们。这是个幼稚的期望。TC39 成员尚未意识到,浏览器实现者们非常不愿意删除网页上实际可能用到的任何特性(不论是否标准化)——某些网页永远不会消失。
在审查并解决了所有未解决的问题后,TC39 一致接受规范,认为它已经完备,遵从并纳入了会议中所提出的更改要求。Waldemar Horwat 和 Herman Venter 准备了最终文档 [TC39 1999e],并于 1999 年 10 月 13 日将其交给了 Ecma 秘书处。最终草案中有一张表,其中列出了 ECMA-262 前三个版本的所有贡献者(图 21),包括内容创作、技术会议参与,以及通过电子邮件的贡献。
Mike Ang | Gary Fisher | Clayton Lewis | Sam Ruby |
Christine Begle | Richard Gabriel | Drew Lytle | Dario Russi |
Norris Boyd | Michael Gardner | Bob Mathis | David Singer |
Carl Cargill | Bill Gibbons | Karl Matzke | Randy Solton |
Andrew Clinick | Richard Gillam | Mike McCabe | Guy Steele |
Donna Converse | Waldemar Horwat | Tom McFarland | Michael Turyn |
Mike Cowlishaw | Shon Katzenberg | Anh Nguyen | Herman Venter |
Chris Dollin | Cedric Krumbein | Brent Noorda | George Wilingmyre |
Jeff Dyer | Mike Ksar | Andy Palay | Scott Wiltamuth |
Brendan Eich | Roger Lawrence | Dave Raggett | Rok Yu |
Chris Espinosa | Steve Leach | Gary 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 就够了。