从 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] 中的桥段,将新引擎命名为「SpiderMonkeyg」27。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]。
* do 语句
* 语句标签,以及 break/continue 到标签
* switch 语句
* 嵌套函数声明(使用词法作用域)
* 函数表达式(lambda 表达式)
* 消除原本由 == 运算符所执行的隐式类型转换
* 可妥善删除属性的 delete 运算符
* 对象字面量
* 数组字面量
* 正则表达式字面量
* 可进行正则表达式匹配的 RegExp 对象
* 所有对象上的 __proto__ 伪属性
* 新数组方法 push, pop, shift, unshift, splice, concat, slice
* 新字符串方法 charCodeAt
* 基于 RegExp 的 fromCharCode (ISO latin-1), match, replace, search, substr, split
* 函数的 arity 属性
* 将函数及其 arguments 对象拆分为不同对象
* 函数的形参与局部声明,可作为 arguments 对象上的属性名
* arguments.callee
* watch/unwatch 函数
* import/export 语句与脚本签名
图 10. JavaScript 1.2 的新特性。
在 JavaScript 1.2 中,大多数新加入的库都来自于其他流行语言现有特性的启发。数组 concat
和 slice
方法的灵感来自 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 中,可以对变量和函数做前向引用,因为语言在逻辑上将顶级的 var
和 function
声明「提升」到了脚本的开头,而函数局部的 var
声明也会被「提升」到函数体的开头。类似地在 JavaScript 1.2 中,嵌套的 function
声明也会被提升到封闭函数体的开头。如果有多个具有相同名称的 function
声明,那么就将封闭函数体源码中最后出现的那个声明与该名称绑定。
JavaScript 1.2 还提供了 lambda 表达式支持,这是通过允许函数定义作为表达式原语的方式来实现的。它们称为「函数表达式」,并在语法上与函数声明相同,只是函数名称变成了可选的。如果存在函数名称,语言则会出于绑定目的,将函数表达式视为提升后的 function
声明。不带函数名称的函数表达式则会定义一个匿名函数。不论在哪种情况下,函数表达式的每次运行时求值都会创建一个新的闭包(closure)。新的 callee
属性被添加到了 arguments
对象上,使得此类闭包可以递归引用自己。
数组字面量和对象字面量28都受到了 Python 中类似特性的启发。数组字面量为创建和初始化数组对象的元素提供了简洁的语法,让 JavaScript 程序员可以编写如下内容:
// JavaScript 1.2
var p2 = [1, 2, 4, 8, 16, 32, 64];
而不必这样:
// JavaScript 1.1
var p2 = new Array();
p2[0] = 1;
p2[1] = 2;
p2[2] = 4;
// etc.
类似地,对象字面量提供了用于创建对象并将属性与之关联的简洁语法。通过对象字面量,程序员可以编写如下内容:
// JavaScript 1.2
var origin = { x: 0, y: 0 };
而不必这样:
// JavaScript 1.0
var origin = new Object;
origin.x = 0;
origin.y = 0;
对象字面量和函数表达式的组合,简化了对包含方法的无类(classless)对象的定义。例如:
// JavaScript 1.2
function Point(x, y) {
return {
x: x,
y: y,
distance: function (another) {
return Math.sqrt(Math.pow(this.x - another.x, 2)
+ Math.pow(this.y - another.y, 2)
);
}
}
}
var origin = new Point(0, 0);
alert(origin.distance(new Point(5, 5)));
将对象字面量和函数表达式的组合,也提供了一种更方便的方法来定义原型对象。另外添加的地方还有 __proto__
伪属性(pesudo-property),这个伪属性使 JavaScript 程序能动态访问并修改每个对象(用来访问继承属性)的内部引用29。通过使用 __proto__
,程序可以动态构造任意深度的属性继承层次结构,并动态指定对象该从何处继承属性。
最终,某些 JavaScript 1.2 的更改被证明是错误的。import
和 export
语句旨在与 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 上十分流行。