从 Mocha 到 SpiderMonkey

在 1995 年全年和 1996 年的大部分时间里,Brendan Eich 都是唯一全职从事 JavaScript 引擎g25开发工作的 Netscape 开发者。在 1996 年 8 月发布的 Netscape 3.0 版本中,JavaScript 1.1 仍然主要包含 1995 年 5 月的 10 天原型代码。在发布这个版本后,Eich 认为是时候偿还引擎g的技术债26,并努力使 JavaScript「成为一门更干净的语言」了。但 Netscape 管理层则希望他研究语言规范。他们对微软针对 JavaScript 规范缺失的批评很敏感,并认为即将开始的语言标准化进程需要这样一份规范作为输入。Eich 拒绝了,他想把重新实现 Mocha 作为开始。要想编写规范,他需要的是仔细检查 Mocha 的实现。他认为在检查 Mocha 时重写 Mocha 是最有效率的方法,这也能让他在初始的设计错误被纳入规范前纠正它们。

由于对辩论感到沮丧,Brendan Eich 离开办公室,在家工作了两个星期。在此期间,他重新设计实现了 JavaScript 引擎的核心。此举的收获是一个更快、更可靠和更灵活的运行引擎。他舍弃了将 JavaScript 值表示为差异联合体g的实践,改为使用包含即时原始值的标记指针(tagged pointer)。他还实现了诸如嵌套函数、函数表达式和 switch 语句之类的特性,这些特性从未在原始引擎中实现过。基于引用计数的内存管理器也被替换成了基于标记 / 清除算法的垃圾收集器。

当 Eich 返回办公室时,新引擎已经取代了 Mocha。Chris Houck 这位早期的 Netscape 开发者也参与了进来,成为了 JavaScript 团队的第二位专职成员。Houck 根据电影《Beavis and Butt-Head Do America》[Judge et al. 1996] 中的桥段,将新引擎命名为「SpiderMonkeyg27。Clayton Lewis 加入团队担任经理,并聘请来了 Norris Boyd。技术作家 Rand McKinny 被派来协助 Eich 编写规范。

Brendan Eich 继续将语言增强为 JavaScript 1.2,以使其成为 Netscape 4.0 的一部分。它于 1996 年 12 月发布了第一个 beta 版本,而正则表达式则添加到了 1997 年 4 月的 beta 版本中。各平台上的 Netscape 4 生产版本于 6 月起开始释出,并于 1997 年下半年进行了分发。

SpiderMonkey 所实现的 JavaScript 1.2 语言和内置库,相对于 JavaScript 1.0/1.1 有了显著的增强。图 10 列出了JavaScript 1.2 中主要的新特性 [Netscape 1997c]。

  1. * do 语句
  2. * 语句标签,以及 break/continue 到标签
  3. * switch 语句
  4. * 嵌套函数声明(使用词法作用域)
  5. * 函数表达式(lambda 表达式)
  6. * 消除原本由 == 运算符所执行的隐式类型转换
  7. * 可妥善删除属性的 delete 运算符
  8. * 对象字面量
  9. * 数组字面量
  10. * 正则表达式字面量
  11. * 可进行正则表达式匹配的 RegExp 对象
  12. * 所有对象上的 __proto__ 伪属性
  13. * 新数组方法 push, pop, shift, unshift, splice, concat, slice
  14. * 新字符串方法 charCodeAt
  15. * 基于 RegExp fromCharCode (ISO latin-1), match, replace, search, substr, split
  16. * 函数的 arity 属性
  17. * 将函数及其 arguments 对象拆分为不同对象
  18. * 函数的形参与局部声明,可作为 arguments 对象上的属性名
  19. * arguments.callee
  20. * watch/unwatch 函数
  21. * import/export 语句与脚本签名

图 10. JavaScript 1.2 的新特性。

在 JavaScript 1.2 中,大多数新加入的库都来自于其他流行语言现有特性的启发。数组 concatslice 方法的灵感来自 Python 的序列操作,而 push / pop / shift / unshift / splice 方法都直接根据同名的 Perl 数组函数建模。Python 还启发了字符串的 concat / slice / search 方法。字符串的 match / replace / substr 来自 Perl。Java 启发了 charCodeAt 方法。至于正则表达式的字符串匹配语法和语义,借鉴的则还是 Perl。

JavaScript 1.2 在语句层面所添加的内容,提供了以前熟悉 C 系列语言的程序员所期望的语句。do 语句直接复制了 C 语言 do 语句的语法和类似的语义,这在 JavaScript 1.0 中遗漏了。带标签的语句以及名为 break / continue 的标签,则是直接按照 Java 中的相同特性建模的。它们允许从多级嵌套的循环和 switch 语句中尽早脱离(early escape),也可以在非迭代的代码块里这么做。JavaScript 1.2 的 switch 语句包含了对 case 选择器表达式的编译期求值 [Eich et al. 1998, jsemit.c lines 757-776],这同 C 与 Java 是一致的。

在 JavaScript 1.0/1.1 中,函数只能定义在脚本顶层的全局声明中。JavaScript 1.2 支持把函数通过局部声明的形式,定义在另一个封闭函数中。这样的内部函数定义可以嵌套到任意层级。内部函数具备词法作用域,它们的局部声明会遮盖外部作用域中具有相同名称的声明。在 JavaScript 1.0/1.1 中,可以对变量和函数做前向引用,因为语言在逻辑上将顶级的 varfunction 声明「提升」到了脚本的开头,而函数局部的 var 声明也会被「提升」到函数体的开头。类似地在 JavaScript 1.2 中,嵌套的 function 声明也会被提升到封闭函数体的开头。如果有多个具有相同名称的 function 声明,那么就将封闭函数体源码中最后出现的那个声明与该名称绑定。

JavaScript 1.2 还提供了 lambda 表达式支持,这是通过允许函数定义作为表达式原语的方式来实现的。它们称为「函数表达式」,并在语法上与函数声明相同,只是函数名称变成了可选的。如果存在函数名称,语言则会出于绑定目的,将函数表达式视为提升后的 function 声明。不带函数名称的函数表达式则会定义一个匿名函数。不论在哪种情况下,函数表达式的每次运行时求值都会创建一个新的闭包(closure)。新的 callee 属性被添加到了 arguments 对象上,使得此类闭包可以递归引用自己。

数组字面量和对象字面量28都受到了 Python 中类似特性的启发。数组字面量为创建和初始化数组对象的元素提供了简洁的语法,让 JavaScript 程序员可以编写如下内容:

  1. // JavaScript 1.2
  2. var p2 = [1, 2, 4, 8, 16, 32, 64];

而不必这样:

  1. // JavaScript 1.1
  2. var p2 = new Array();
  3. p2[0] = 1;
  4. p2[1] = 2;
  5. p2[2] = 4;
  6. // etc.

类似地,对象字面量提供了用于创建对象并将属性与之关联的简洁语法。通过对象字面量,程序员可以编写如下内容:

  1. // JavaScript 1.2
  2. var origin = { x: 0, y: 0 };

而不必这样:

  1. // JavaScript 1.0
  2. var origin = new Object;
  3. origin.x = 0;
  4. origin.y = 0;

对象字面量和函数表达式的组合,简化了对包含方法的无类(classless)对象的定义。例如:

  1. // JavaScript 1.2
  2. function Point(x, y) {
  3. return {
  4. x: x,
  5. y: y,
  6. distance: function (another) {
  7. return Math.sqrt(Math.pow(this.x - another.x, 2)
  8. + Math.pow(this.y - another.y, 2)
  9. );
  10. }
  11. }
  12. }
  13. var origin = new Point(0, 0);
  14. alert(origin.distance(new Point(5, 5)));

将对象字面量和函数表达式的组合,也提供了一种更方便的方法来定义原型对象。另外添加的地方还有 __proto__ 伪属性(pesudo-property),这个伪属性使 JavaScript 程序能动态访问并修改每个对象(用来访问继承属性)的内部引用29。通过使用 __proto__,程序可以动态构造任意深度的属性继承层次结构,并动态指定对象该从何处继承属性。

最终,某些 JavaScript 1.2 的更改被证明是错误的。importexport 语句旨在与 Netscape 4 中兼容 Java 的脚本签名机制 [Netscape 1997a] 一起使用。对于签名后的脚本,它们之中定义的全局变量对该脚本是私有的,但使用 export 语句可以显式导出其中的函数。非 Netscape 浏览器从未采用过此特性。

尽管用户需求促生了 JavaScript 1.0/1.1 中 == 运算符的隐式类型转换规则,但一些用户仍发现该行为令人惊讶和混乱。Brendan Eich 决定消除 JavaScript 的大多数隐式类型转换,以修复 == [Netscape 1997a; Rein 1997]。如果两个操作数都不是相同的原始类型(数字,字符串,布尔值,对象),那么 == 将返回 false

JavaScript 1.2 希望通过 <script> 标签的 version 属性,来应对 JavaScript 1.0 和 1.1 的语义更改。但是到 JavaScript 1.2 生产版本发布时,这种形式的版本管理对 Web 开发者来说已变得难以维护 [Rein 1997],对于需要工作在非 Netscape 浏览器上的网页来说尤其是这样。这些浏览器都维护了自己的 JavaScript 实现。

插曲:风评被害

从诞生之初,JavaScript 一直受到舆论的激烈批评。一些批评针对的是这门语言基本的设计决策,例如动态类型或隐式类型转换等设计细节。其他批评者对于它与 HTML 的集成方式,或对于它暴露浏览器安全漏洞的风险 [Fair 1998],也存在着巨大的反对意见。Robert Cailliau [Wikinews 2007] 称 JavaScript 为「计算史上最可怕的糟粕」,并说:「我只知道一种比 C 更糟糕的编程语言,那就是 Javascript(原文如此)。」Bret Bos 在 W3C 研讨会上 [2005] 将 JavaScript 描述为「有史以来最糟糕的发明」。

对许多新手程序员而言,浏览器中的 JavaScript 让他们首次接触到了常见的编程问题,例如浮点运算的挑战等。他们通常认为这些问题是 JavaScript 特有的。许多经验丰富的程序员将 JavaScript 与熟悉的编程语言(或由于名称混淆而与 Java)进行比较,并发现 JavaScript 的不足。介绍 JavaScript 怪癖的文章 [Cardy 2011] 以及相关网站(例如 wtfjs.com [Leroux 2010])一度在 Web 上十分流行。