JavaScript 从未打算成为今天的样子。1995 年,Brendan Eich 在 Netscape 工作,仅用十天时间就敲出了一个原型。十天。这时间甚至勉强够写出一份像样的规范,更不用说设计一种旨在驱动数十亿台设备的编程语言了。其结果是一种充满了让开发者在近三十年后仍会跌跤的怪癖(quirks)的语言。然而,正是这种仓促的产物,成为了软件史上部署最广泛的运行时(runtime)。它的成功并非因为其优雅,而是因为它恰好在 Web 需要它的时刻,随浏览器一同发布了。
仓促诞生
九十年代中期的浏览器大战并非友好的竞争。Netscape 需要一种轻量级的脚本语言,可以在其 Navigator 浏览器中与 Java 并行运行。Eich 的任务是构建一些看起来足够像 Java 以安抚高层,但又足够简单,能让非程序员直接粘贴到网页中的东西。截止日期极其荒谬。他产出了 Mocha,随后更名为 LiveScript,最后为了蹭 Java 的热度,将其更名为 JavaScript 作为营销手段。
这种匆忙的诞生留下了永久的伤痕。类型转换(Type coercion)至今仍会让新手感到困惑,比如加号运算符会在没有任何警告的情况下将字符串和数字进行拼接。typeof null 返回 "object" 是因为原始实现中的一个 bug,由于担心破坏 Web 生态,至今没人敢修复它。自动分号插入(Automatic semicolon insertion)会导致静默失败。使用 var 声明的变量其作用域泄露的方式也让人感到难以预测。这些并非抽象的设计缺陷,而是可以直接追溯到 1995 年 5 月那场为期两周冲刺的日常挫败感。
奇特的血统
仔细观察 JavaScript,你会发现它是由三个截然不同的血统缝合而成的。语法大量借鉴了 Java。大括号、if 语句和 for 循环为任何熟悉 C 风格语言的人提供了熟悉的形态。但在那层表象之下,其行为却完全不同。
该语言真正的计算核心来自 Scheme(Lisp 的一种方言)。JavaScript 的一等函数(first-class functions)便源于此,这意味着函数可以作为参数传递、从其他函数返回,并赋值给变量。它还赋予了我们闭包(closures),允许内部函数在外部函数执行完毕后仍能访问其作用域。如果你写过回调函数(callback)或附加过事件监听器(event listener),你就已经使用了继承自 Scheme 的 DNA。
然后是对象模型,它来自 Self。JavaScript 没有使用带有严格类的经典继承,而是使用了原型(prototypes)。一个对象可以直接链接到另一个对象,并向上委托属性查找。你可以使用 Object.create 创建一个对象并构建原型链,而无需定义类。现代 JavaScript 添加了 class 关键字,但这在很大程度上只是底层原型机制的语法糖。
从页面装饰到严肃工具
在最初的几年里,JavaScript 只负责一些小工作。它在表单提交到服务器之前验证输入。它在鼠标悬停时切换图片。它是一个玩具,而不是工具。由于浏览器的实现并不一致,开发者经常需要为 Netscape 和 Internet Explorer 编写不同的代码路径。
通过 ECMAScript 实现的标准化改变了这一轨迹。规范为浏览器厂商提供了一个共同的实现目标,从而慢慢磨平了最严重的兼容性问题。随后,Ajax 出现了。
Ajax(Asynchronous JavaScript and XML 的缩写)并不是一种单一的新技术,而是一种结合了现有组件的模式。其关键要素是 XMLHttpRequest 对象,它允许浏览器在后台向服务器请求数据,而无需重新加载整个页面。当 Google 在 2005 年推出 Maps 和在 2004 年推出 Gmail 时,用户突然在浏览器标签页中体验到了类似桌面端的响应速度。网页变成了应用程序。JavaScript 不再是点缀,而是主菜。
速度与雄心
原始性能曾是 JavaScript 最大的笑柄。早期的解释器非常慢。直到 2008 年 Google 随 Chrome 发布了 V8 引擎,这个笑话才不再好笑。V8 引入了即时编译(just-in-time compilation),在运行时将 JavaScript 转换为机器码,而不是逐行解释。它引入了隐藏类(hidden classes)和内联缓存(inline caching),使得即使在动态对象上也能实现快速的属性访问。其他浏览器也纷纷推出了自己的高速引擎,该语言突然变得足以进行真正的计算。
这种速度促成了下一次变革。Ryan Dahl 在 2009 年发布了 Node.js,将 V8 从浏览器中剥离出来,并将其封装在一个事件驱动、非阻塞的输入/输出模型中。Web 服务器过去会为每个传入的请求创建一个新线程,这在面对高并发时会崩溃。Node.js 利用事件循环和异步回调,在单个线程上处理了数以万计的并发连接。JavaScript 从客户端蔓延到了服务端、构建工具,并最终渗透到了其他一切领域。
无处不在的语言
现在,JavaScript 运行在它的创造者从未想象过的地方。
在前端,React 和 Vue 塑造了现代界面的构建方式。组件响应状态变化进行更新,而无需浏览器执行昂贵的整页重载。在后端,Node.js 为 API 和实时服务提供动力,而像 Bun 这样较新的运行时则在尝试更快的包管理和内置打包功能。
React Native 将 JavaScript 代码转换为原生平台视图,让团队无需分别维护 Swift 和 Kotlin 两套完全独立的 codebase,即可发布 iOS 和 Android 的移动应用。Electron 将 Web 技术封装在 Chromium 外壳中以构建桌面软件,Slack 和 Visual Studio Code 正是如此运行在你的笔记本电脑上的。这种语言甚至存在于云函数和边缘计算 Worker 中,通过分布式网络在距离终端用户仅几毫秒的地方执行逻辑。
丰饶的悖论
无处不在是有代价的。生态系统极其庞大,而这种规模也滋生了焦虑。在你还没配置完上一个构建工具时,一个新的构建工具就出现了。框架的流行程度起伏不定,其周期感仿佛季节更替。你几乎可以在不离开 JavaScript 世界的情况下解决任何问题,但你必须首先从十几种极具主观倾向的解决方案中做出选择。依赖树变得深厚且脆弱。一个用于数组左填充的包,一旦从注册表中消失,可能会导致数千个下游项目崩溃。
这并非偶然。JavaScript 之之所以混乱,是因为 Web 本身就是混乱的。它是由向后兼容性、仓促制定的标准以及相互竞争的实现利益交织而成的有机“层叠蛋糕”。然而,正是这种混乱让这门语言变得强大。Web 无处不在,而由于 JavaScript 默认存在于每个浏览器中,它成为了我们所拥有的最接近“通用运行时”的东西。
它早在很久以前就不再仅仅是一种脚本语言了。现在的 JavaScript 是一个全球性的软件平台,它诞生于短短十天,并由一个无形的契约维系着:Web 绝不能破坏既有的功能。在学习其优势的同时,去了解它的伤痕,你便是在学习现代互联网本身的历史。
