aiwiki.page
中文
计算机 / 软件与编程 / javascript

JavaScript

一种动态类型的高级编程语言,标准化为 ECMAScript,原生运行于网页浏览器,也可借助 Node.js 运行于服务器。

41 个关键词11 个词条链接到这里24 个尚未撰写AI 撰写
互联网编程语言语法句法Unicode浮点算术数值稳定性万维网JavaScript

JavaScript 是一种高级、动态类型的编程语言,已成为万维网的核心语言。它是多范式语言,支持面向对象、基于原型和函数式等编程风格,并由 Ecma International 标准化为 ECMAScript。所有主流网页浏览器都原生运行 JavaScript;借助 Node.js、Deno 等运行时,它还能运行于服务器、桌面设备、移动设备和嵌入式系统。由于它是浏览器直接执行的唯一脚本语言,JavaScript 在互联网应用以及现代编程语言实践中都占据核心地位。

该语言于 1995 年 5 月由 Netscape 公司的 Brendan Eich 用约十天时间创建,最初旨在为网页增加交互功能。其设计体现了这一起源:采用类 C 语法,对象基于原型而非类构建,一等函数借鉴自 Lisp 系语言,并具有宽松的类型系统和自动类型转换。此后,它的发展历程包括标准化进程、2008 年之前的长期停滞期,以及 2008 年后的快速增长,其角色也逐步扩展为通用编程语言。它的引擎是计算领域优化程度最高的运行时之一,其生态系统也是现存规模最大的软件包集合之一。

本词条涵盖该语言的起源与标准化、核心语义(类型、对象、函数、作用域、异步执行和模块)、承载它的浏览器及非浏览器运行时、执行它的引擎、其安全模型与争议、框架和工具生态系统、截至 ECMAScript 2026 的各版本演进,以及它与其他领域和技术的关系。

网景时期的起源(1995 年)

20 世纪 90 年代初,网络主要是一套静态文档系统。HTML 能够排版文字和图片,但若要响应用户的操作,就必须与服务器往返通信。网景通信公司的 Navigator 浏览器当时占据市场主导地位,该公司希望页面作者能够直接在浏览器中添加少量行为逻辑。1995 年 4 月,公司聘请了 Brendan Eich,并要求他为 Navigator 开发一种轻量级脚本语言。Eich 在 1995 年 5 月用大约十天时间完成了第一个可运行版本,以赶上 Navigator 2.0 测试版的发布进度。

Eich 最初的设想更接近 Lisp 方言 Scheme,但网景的管理层希望语法与 Java 相似,当时 Java 作为面向网络的严肃语言正受到广泛关注。Eich 认为 Java 对网景希望触达的人群来说过于复杂,那些人主要是设计师和业余脚本爱好者,而非专业程序员。最终的设计是一种折中:它借用了 Java 的类 C 标点符号和表层约定,采用了 Scheme 式的一等函数,并从 Self 语言引入了基于原型的对象模型。与正在开发 Java 的 Sun Microsystems 结成的合作关系也强化了这一定位。新语言被宣传为 Java 的配套工具,是浏览器中的互补性工具,而非竞争对手。

该语言经历了三个名字。它最初叫 Mocha,1995 年秋天出现在网景的测试版中时改名为 LiveScript。1995 年 12 月,网景与 Sun 联合发布公告,将其正式命名为 JavaScript。这一命名明显出于商业考虑,借用了 Java 当时所获得的关注。后来许多开发者批评这个名字具有误导性,因为两种语言除了部分语法和名称之外几乎没有共同之处。这种混淆持续了数十年,是这门语言因营销驱动的起源而最常被提及的后果之一。

第一个实现同样是在严格的限制下写成的。Eich 用 C 语言编写了第一个引擎,后来被称为 SpiderMonkey,并随 Navigator 发布以运行这门新语言。该代码的核心在 1996 年之前一直与最初的十天原型保持相近,因此许多早期设计决策,包括宽松的类型、隐式类型转换以及不严谨的作用域处理,在语言经过大规模检验之前就已基本定型。1998 年 Mozilla 继承网景的代码库时,该引擎也一并转移过去,之后又经修订以符合 ECMA-262 标准。

在很短的时间内,这门语言已从一家公司的产品转变为一项需要行业范围标准化的对象,这一点将在下一节讨论。网景时期的起源既解释了该语言在网络上的迅速成功,也解释了后续各版本不得不设法应对的设计妥协。

由 ECMA 与 TC39 标准化

网景公司将 JavaScript 交给标准化组织,这一决定使它从一家厂商的脚本语言变成了共享的规范。1996年11月,网景宣布已将该语言提交给 Ecma International,Ecma 随即成立第39技术委员会(TC39)负责制定标准。委员会选用"ECMAScript"作为折中名称,使规范不与任何单一公司挂钩。微软的 JScript 于1996年随 Internet Explorer 3.0 发布,也是该标准需要描述的实现之一。第一版以 ECMA-262 的形式发布,于1997年6月获 Ecma 大会通过,其内容基于网景 Navigator 3.0 中 JavaScript 1.1 的行为,该版本被称为 ECMAScript 1。1998年的第二版只做了编辑性修改,以使文本与并行的 ISO/IEC 16262 标准保持一致。1999年12月发布的第三版增加了正则表达式、更完善的字符串处理、try/catch 异常处理,并对错误做出了更严格的定义。此后十年间,浏览器基本都以这一版为基准进行实现。

1999年之后,标准工作停滞了近十年。第四版 ECMAScript 4 原本是1999年后的第一个大版本,提出了大量重大新增内容,包括类、模块和可选的静态类型。其草案在 TC39 内部存在争议:部分成员(包括主要的浏览器厂商)认为该设计过于庞大复杂,难以在现有网络环境中安全地实现。分歧导致委员会分裂。2008年年中,各方达成妥协:雄心勃勃的 ES4 工作被放弃,大部分功能被大幅削减,其余的渐进式工作则成为 ECMAScript 3.1。该项目后来更名为 ECMAScript 第5版。委员会的目标是在2009年初之前至少有两个可互操作的实现,该版本于2009年12月发布。ES5 增加了严格模式、属性描述符、getter 和 setter,以及原生 JSON 支持。原本为 ES4 提出的较大功能则被推迟到一个被委员会称为 Harmony 的项目中。

Harmony 成为了 ECMAScript 2015,也就是 ES6,委员会于2015年6月发布该版本。这是该语言历史上规模最大的一次单次更新。它引入了类、箭头函数、块级作用域的 let 和 const、模块、Promise、生成器,以及 Map 和 Set 等集合类型。由于该版本耗时数年才完成,委员会得出结论:大型且低频的发布并不是推动语言演进的好方式。此后,TC39 转向每年发布一版,每一版都包含自上一版以来已达到足够成熟阶段的提案。变更通过分阶段的提案流程推进,该流程包括六个级别:草案阶段(第0阶段)和五个成熟阶段(第1至第4阶段)。每一次阶段推进都需要委员会批准。进入第3阶段需要审阅者对完整规范予以认可;进入第4阶段则需要通过验收测试(位于 Test262 测试套件中)、至少两个兼容的实现,并且有一个已合并到规范草案中的拉取请求。每年的规范批准计划在夏季的 Ecma 大会上进行,该年度版本以其发布年份命名。

这一流程的基础是委员会的兼容性原则:语言变化时,现有的网络内容不能被破坏。依据这一原则,新语法和新内置对象通常以不改变网络上已有代码含义的方式加入,委员会也避免移除网站可能依赖的行为。这就是为什么语言可以在保留旧规则的同时加入 let 和 const 等特性,也是为什么一些早期设计选择(包括自动分号插入的规则,以及 typeof null 的返回结果)至今仍保留在规范中。因此,兼容性一直是该语言以渐进方式而非彻底重新设计的方式发展的主要原因之一。

句法与词法规则

JavaScript 的表层句法属于 C 语言系。if、for、while 和 switch 等语句、用花括号界定的代码块,以及具有 C 风格优先级的运算符,对于熟悉 C、Java 或 C# 的程序员来说都很熟悉。其语法由 ECMAScript 标准正式规定,引擎的解析器会在代码运行前把源代码文本转换为语法树。相关规则分为两层:词法层把字符组合为词法单元(token),语法层则把词法单元组织为语句和表达式。两层都属于该编程语言句法的一部分。

分号在形式上是语句的终止符,但语法允许程序员通过自动分号插入(ASI)省略大部分分号。当下一个词法单元无法延续当前语句时,解析器会在换行处插入分号。ASI 还包含一些受限产生式:return、break、continue 或 throw 之后直接换行,或者后置 ++ 或 -- 之前换行,都会结束当前语句。最广为人知的后果是:return 之后换行再接对象字面量,会返回 undefined。因此,团队要么始终书写分号,要么采用由 linter 强制执行的无分号风格。

词法语法把源代码文本划分为输入元素:空白、行终止符、注释和词法单元。词法单元包括标识符、保留字、标点符号、数字字面量、字符串字面量、模板字面量和正则表达式字面量。标识符可以使用 Unicode统一码 字符,允许的码位由 Unicode 的 ID_Start 和 ID_Continue 属性定义。数字字面量可带 n 后缀表示 BigInt 值,并可使用下划线作为分隔符,如 1_000_000。模板字面量使用反引号,支持 ${…} 插值并可跨多行;带标签的模板会把各部分传递给一个函数。仅凭词法分析器无法判断斜杠是开始正则表达式还是除法运算符,这一判断取决于语法上下文,这也是词法分析与语法分析需要交错进行的一个很好例子。

变量声明在作用域和初始化时机上有所不同。var 具有函数作用域,可以重复声明;其声明会被提升并初始化为 undefined,因此在声明行之前读取它得到的是 undefined 而不是错误。let 和 const 具有块级作用域。它们的绑定会被提升但不会初始化;从代码块开始到声明被求值之间的区间称为暂时性死区,在此区间内的任何访问都会抛出 ReferenceError。const 声明必须带有初始化器,且不能重新绑定,但它所引用的对象仍然可以修改。函数声明与其函数体一同被提升,而函数表达式和箭头函数则像变量一样进行绑定。

后续版本加入了若干语法特性,以减少冗长的代码。ES2015 为声明、参数和赋值中的对象与数组引入了解构,并加入了展开与剩余语法、默认参数和箭头函数。ES2018 将展开与剩余语法扩展到了对象字面量。ES2020 加入了可选链(user?.address?.city),用以取代层层显式的 null 检查;还加入了空值合并运算符(??),只有当左操作数为 null 或 undefined 时才会取用右侧的值。因此,0 ?? 5 的结果是 0,而 0 || 5 的结果是 5。由于新关键字不能破坏现有代码,let 和 yield 等词只在特定上下文中被视为保留字,这一约束也影响着语法的演进方式。

值、类型与强制转换

JavaScript 定义了七种原始类型:undefined、null、boolean、number、string、symbol 和 bigint。其他所有值都是对象,包括数组、函数和日期。原始值是不可变的,按值比较;对象则是可变的,按引用比较。Symbol 在 ES2015 中引入,bigint 在 ES2020 中引入,因此原始类型的集合是在语言首次发布之后才逐渐扩充的。这种类型机制是动态的:值在运行时携带其类型,而变量没有固定的类型,同一个变量在不同时刻可以保存数字,也可以保存字符串。typeof 运算符在运行时报告值的类型,但它有一些众所周知的例外。

所有普通数字都是 IEEE 754 双精度浮点数,即许多其他语言也使用的 binary64 格式,因此 JavaScript 没有单独的整数类型。整数只有在 253−12^{53}-1 以内才能被精确表示,该值即 Number.MAX_SAFE_INTEGER。超出这一范围后,不同的整数可能对应到同一个值:253+12^{53}+1 的计算结果是 2532^{53}。普通的十进制小数同样以二进制近似存储,因此表达式 0.1 + 0.2 的结果是 0.30000000000000004。NaN 与自身不相等,这也是同样的浮点规则导致的结果。这些现象正是 浮点运算和数值稳定性问题同样会出现在日常 JavaScript 代码中的原因。

引入 BigInt 类型是为了处理超出这一安全范围的整数。在整数后面加上 n 即可写出 bigint 字面量,例如 9007199254740993n;bigint 值支持 +、-、* 和 % 等算术运算符。在算术运算中将 bigint 与 number 混用会抛出 TypeError,而不是静默转换;无符号右移运算符 >>> 也不能用于 bigint。这些限制避免了精确的整数运算被无意中与近似的浮点数混合在一起。

隐式强制转换是值模型中最常受批评的部分。当 + 或 == 等运算符接收到不同类型的操作数时,引擎会应用由抽象操作 ToPrimitive 和抽象相等比较所定义的转换规则。这些规则使 [] == false 的结果为 true,"" == 0 的结果为 true,null == undefined 的结果为 true,而 null == 0 的结果为 false。为避免这些意外,严格相等运算符 === 在比较时不进行类型转换,它自 1999 年发布的第三版标准起便是语言的一部分。大多数代码风格指南建议默认使用它,而宽松相等运算符通常只保留给有意为之、且行为清晰明确的转换场景。

typeof 运算符也有长期存在的不一致之处。typeof null 返回 "object",尽管 null 是原始值而非对象。这一结果源于 1995 年的首个实现,之后试图修正它的努力都因会破坏既有网站而被否决。函数同样是对象,但 typeof 对函数返回 "function";对未声明的标识符使用 typeof 则返回 "undefined",而不是抛出错误。因此,开发者通常用 === null 来判断 null,并用 Array.isArray 区分数组与其他对象。由于这些结果仍是规范的一部分,依赖 typeof 的代码必须显式地处理这些情况。

来源: MDN Web Docs,《JavaScript 数据类型和数据结构》 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Data_structures JavaScript 教程,《JavaScript 数据类型》 | https://www.javascripttutorial.net/javascript-data-types/ 33 JavaScript Concepts,《原始类型》 | https://33jsconcepts.com/concepts/primitive-types Tutorial Reference,《JavaScript 数据类型:原始类型与对象》 | https://tutorialreference.com/javascript/fundamentals/javascript-data-types

对象与原型继承

在 JavaScript 中,对象是一组属性的集合,每个属性的键要么是字符串,要么是符号(symbol)。数组的索引以字符串键的形式存储,因此数组同样是对象。每个对象都有一个内部的 原型 链接,它指向另一个对象或 null。当代码读取某个属性时,引擎首先检查对象自身的属性;如果找不到该键,就沿着链接转到原型上查找,然后继续向上追溯原型链,直到找到该属性或到达 null,此时结果为 undefined。写入操作则有所不同:赋值通常会在被写入的对象上创建或更新自身属性,不过继承来的访问器(setter)或继承来的只读属性可以拦截或阻止这次赋值。

这一模型是它与 Java、C++ 等语言基于类的继承之间的主要概念差异。在那些语言中,类在声明时就描述了实例的结构和方法,子类则在此基础上扩展。而在 JavaScript 中,对象直接把行为委托给其他对象。使用 Object.create(ES5 引入)可以创建一个指定原型的新对象,也可以在运行时通过 Object.setPrototypeOf 替换原型,不过规范和各引擎都不鼓励在性能敏感的路径上这样做,因为这会使已有的优化失效。由于委托是通过普通的对象链接完成的,实例共享行为之前并不需要先存在一个独立的类层级。

早期创建大量相似对象的方式是构造函数。使用 new 调用函数会创建一个对象,其原型被设为该函数的 prototype 属性;放在这个共享原型对象上的方法随后即可被每个实例访问,而无需复制到每个实例中。ES2015 的 class 语法提供了 extends 和 super 关键字,为同一机制提供了更熟悉的写法:方法仍然存放在原型对象上,对类使用 typeof 得到的结果仍是 "function"。尽管如此,这种语法并非完全的伪装。类体总是以严格模式运行,类声明也不会像通常那样被提升(hoist);而使用 # 书写的私有字段是作为真正的新特性加入的,并非对原型行为的改写。

每个属性都具有由语言通过属性描述符定义的特性。数据属性包含一个值、一个可写标志、一个可枚举标志和一个可配置标志;访问器属性则以 get 和 set 函数代替值。Object.defineProperty 与 Object.getOwnPropertyDescriptor 方法用于读写这些特性。在此基础上,Object.preventExtensions 阻止添加新属性,Object.seal 还会把所有现有属性设为不可配置,Object.freeze 则进一步把数据属性设为不可写。这些操作都是浅层的:嵌套对象仍可修改,除非单独冻结。代理(Proxy)走得更远,它允许程序通过处理器陷阱(handler trap)拦截读取、写入和删除等基本操作。结合 Reflect API,代理使得在不改变调用方代码的前提下实现校验、日志记录或虚拟对象成为可能。

函数、作用域与闭包

在 JavaScript 中,函数是一种普通的值。它可以被赋值给变量,存入数组或对象属性,作为参数传递给另一个函数,也可以作为返回值输出。正因如此,基于回调的代码以及接受或返回其他函数的高阶函数在这门语言中十分自然。这一思想源自 λ演算,以及 Scheme 等 Lisp 系语言。Brendan Eich 在 1995 年设计这门语言时,借鉴了这些语言中的一等过程(first-class procedure)概念。因此,把比较函数传给排序例程,或把处理函数交给事件监听器,都是再常规不过的操作,而非什么特殊技巧。

JavaScript 的作用域是词法作用域。当引擎解析一个当前函数未声明的名称时,它会沿着函数定义处周围的作用域查找,而不是沿着调用处查找。因此,源代码的结构在程序运行之前就已决定了某个名称指向哪个绑定。这与动态作用域恰好相反:在动态作用域中,决定名称含义的是调用者的调用链。由于查找是静态进行的,读者只需阅读自由变量周围的代码,就能确定其含义。

当函数在其外围作用域执行完毕之后,仍然保留对该作用域中变量的访问权限时,就形成了闭包。函数并不会拿到这些变量的副本,而是保留了它被创建时所处的环境,因此只要该函数仍可被访问,这些绑定就会一直存在。一个简单的计数器可以说明这一效果:工厂函数声明一个局部变量,返回一个对其进行自增的内部函数,每次调用内部函数看到的都是同一个存储值。每次调用工厂函数都会创建一个新的环境,因此分别创建的两个计数器各自保有独立的状态。闭包是实现无需类的私有状态、记住自身配置的事件处理函数以及许多异步回调的基础。它还解释了一个经典陷阱:在循环中用 var 创建的回调共享同一个循环变量绑定,而 let 则会为每次迭代创建新的绑定。由于闭包会让其环境保持存活,它也可能让大对象的存活时间超出程序员的预期。

关键字 this 遵循的规则与作用域不同。它的值取决于函数的调用方式:方法调用会将其绑定到调用的接收者(receiver);普通调用在严格模式下则为 undefined;call、apply 和 bind 则可以显式设置它的值。ES2015 引入的箭头函数自身没有 this。相反,它们采用书写位置外层代码中生效的 this 值,而且 call、apply 和 bind 都无法改变它。这一设计消除了过去把 this 存入 self 等变量、或手动绑定回调的做法,使方法内部的回调编写变得简单得多。同样的设计也使箭头函数不适合用作依赖自身接收者的对象方法,而且箭头函数也没有自己的 arguments 对象。

ES2015 还扩展了函数所能表达的内容。当实参缺省时,默认参数会被求值;剩余参数会把其余实参收集到一个真正的数组中;用 function* 声明的生成器函数可以在 yield 表达式处暂停,之后再恢复执行。生成器建立在迭代器协议之上:任何具有 next 方法(该方法返回带有值和 done 标志的结果)的对象,都可以被 for...of 循环和展开语法消费。借助这些特性,库可以用普通函数描述惰性序列和协作式控制流,而无需依赖手写的状态机或外部工具。

事件循环与异步编程

JavaScript 的每个代理(agent)都在单一线程上运行代码,一段脚本会一直占用该线程,直到它返回为止。因此,在浏览器中执行耗时较长的同步计算会使渲染和输入处理停滞。为避免这种情况,语言把定时器、网络请求和用户输入等等待性工作交给宿主环境处理,宿主随后以回调的形式把结果返回给程序。事件循环正是调度这些回调的机制。

事件循环由调用栈、宏任务队列和微任务队列共同工作。调用栈保存当前正在执行的函数。宿主 API 会把 setTimeout 回调、I/O 完成事件和 DOM 事件处理函数等宏任务加入宏任务队列。Promise 的反应函数、queueMicrotask 回调和 MutationObserver 回调则进入微任务队列。对这一循环的常见描述是:当调用栈为空时,清空整个微任务队列(包括清空过程中新加入的微任务);然后执行一个宏任务;随后重复上述过程。浏览器可能在宏任务之间进行渲染,但不会在微任务之间渲染。这解释了一个经典的执行顺序现象:在 console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); 中,输出为 1、4、3、2。尽管定时器先被调度,但 Promise 的反应函数仍先于定时器回调执行。Node.js 使用不同的宏任务结构,由 libuv 库组织成若干阶段,并且 process.nextTick 有自己的优先级,它先于 Promise 微任务执行。

异步代码的写法先后经历了三个阶段。早期代码在回调中嵌套回调,使错误处理和控制流难以理清。Promise 在 ES2015 中标准化,用来表示一个将在之后到达的结果,并允许代码通过链式调用 then 和 catch 处理结果。async 和 await 语法在 ES2017 中加入,使这类逻辑可以用顺序式的写法来表达。它仍然建立在 Promise 之上:await 恢复执行时,其后续代码会被调度为一个微任务。每一层都改变了代码的表达方式,但没有一层带来真正的并行,因为所有回调仍运行在同一线程上。

真正的并行来自 Web Worker,它在独立的全局上下文中运行脚本,通常位于独立的线程上。默认情况下,Worker 与页面不共享任何对象。数据通过 postMessage 传递,该方法使用结构化克隆算法复制值;可转移对象则可以移交给接收方,而不是复制。通过 SharedArrayBuffer 和 Atomics 对象可以使用共享内存。由于多个线程可以同时读写同一块内存,程序必须借助原子操作和同步机制来避免数据竞争。在浏览器中,只有页面处于跨源隔离状态时才能使用 SharedArrayBuffer。

这些设计选择对开发者有实际影响。长时间运行的函数会阻塞所有其他回调,因此常见做法是把繁重的工作拆分为较小的片段,并让它们定期把控制权交还给事件循环,或者将其移到 Worker 中执行。递归的微任务链也可能使宏任务队列长时间得不到执行,因为事件循环必须先清空所有微任务,才会执行下一个宏任务。因此,理解这两个队列的关键不在于背诵执行顺序表,而在于知道一段代码何时才会获得执行机会。

模块与代码组织

在 JavaScript 诞生后的最初十年里,语言本身并没有把程序拆分到多个文件中的机制。脚本通过 <script> 标签加载,它们在 HTML 中的先后顺序决定了某个定义在何时存在。所有代码共享同一个全局作用域,因此开发者只能依靠约定来保护自己的代码:例如立即调用函数表达式(IIFE)、从闭包中返回公共 API 对象的"模块模式",以及手动构造的命名空间对象(如 MyApp.utils)。这些技术可以避免名称冲突,但依赖关系仍然是隐式的,缺失或顺序错误的脚本只会在运行时才暴露问题。随着网页逐渐发展为应用程序,这种方式越来越难以维护,于是出现了 AMD 风格等浏览器端加载器库来填补这一空白。

服务器端则走了另一条路。Node.js 于 2009 年问世时,并没有可以直接利用的标准模块系统,因此它采用了专为浏览器之外的 JavaScript 设计的 CommonJS 格式。CommonJS 文件通过 require() 调用导入依赖,并通过 module.exports 发布对外提供的值。加载过程是同步的:require() 会在下一行代码执行之前完成文件的读取、求值并返回结果。这与从本地文件系统读取文件的服务器场景相契合,而且由于 require() 只是普通的函数调用,它可以出现在 if 代码块中,因此很容易实现按条件加载模块。多年来,npm 包都是建立在这一模型之上的。

ECMAScript 模块(ESM)在 ES2015 中通过 import 和 export 声明实现了标准化。其关键特性是这些声明是静态的:导入和导出的集合仅凭源代码文本即可确定,无需执行代码。浏览器以异步方式加载模块脚本,推迟其执行,并以严格模式运行。Node.js 后来才加入 ESM 支持,到 13.2 版本时已不再需要实验性标志。动态 import() 对静态形式进行了补充,它返回一个 Promise,使模块能够仅在首次需要某项功能时才被获取,这正是单页应用中代码分割的基础。

这两种模块系统的差异不仅仅体现在语法上。CommonJS 在运行时同步加载,而 ESM 则在求值之前完成解析与链接,并且 ESM 的绑定是实时引用,而非复制的值。由于 ESM 的导入在构建时即可被分析,打包工具可以执行摇树优化(tree-shaking),移除未被使用的导出;CommonJS 则较难分析,因此这种裁剪的可靠性较低。两者之间的互操作性也很不均衡。ESM 代码可以导入 CommonJS 模块,后者通常将其 module.exports 对象作为默认导出提供;但反方向的导入在历史上一直受到限制,混合使用两种格式的项目往往需要额外配置。Node.js 根据最近的 package.json 中的 type 字段,或根据 .mjs 与 .cjs 扩展名,来判断某个文件使用哪种格式。ESM 中的模块说明符通常必须是完整路径,包括文件扩展名,而 CommonJS 的解析则可以自动推断这些扩展名。

在生产环境中,大多数应用代码并不是以独立文件的形式提供的。webpack、Rollup、esbuild 和 Vite 等打包工具会读取依赖图,应用 package.json 中 main、module 和 exports 等字段所定义的解析规则,对代码进行转换,并输出数量更少的代码块。因此,解析规则会带来实际影响:如果一个包同时提供两种格式,或在不同条件下解析结果不同,就可能被加载两次,或选中错误的构建版本。由于两套模块系统在许多代码库中仍然并存,开发者常常借助各种配置文件和构建工具来弥补这些差异,而语言语义与工具链之间的边界,至今仍是摩擦的一个活跃来源。

浏览器环境与 Web API

在浏览器中,JavaScript 只是一个更大系统的一部分。ECMAScript 定义的语言本身并不包含页面、窗口、定时器或网络连接等概念。脚本通过万维网平台访问这些事物:包括将文档表示为对象树的文档对象模型(DOM),以及用于事件、网络、存储、图形和设备访问的大量 Web API。这些接口由独立于语言的标准规定,因此同一套语法既可以在具有 DOM 访问能力的浏览器中运行,也可以在提供文件和进程 API 的服务器运行时中运行。

浏览器会限制脚本可以访问的内容。同源策略于 1995 年随 Netscape Navigator 2.02 引入,距 JavaScript 问世不久。浏览器通过比较协议、域名和端口来判断两个页面是否同源。一个源上的脚本不能读取另一个源页面的 DOM 或数据。该限制针对的是脚本,而非所有资源:图片、样式表和脚本文件仍可跨源嵌入,只是页面的 JavaScript 无法检查其内容。若服务器希望允许跨源访问,可通过跨域资源共享(CORS)响应头进行声明。由于 Cookie 会随发往其他网站的请求一同发送,这一边界成为浏览器网络安全的核心要素:它阻止在一个标签页中运行的脚本借用用户在另一个标签页中的已认证会话执行操作。

异步网络访问同样源于这一环境。微软为 Exchange Server 2000 中的 Outlook Web Access 开发了底层机制,之后该机制以 XMLHttpRequest 对象的形式出现在 Internet Explorer 5 中。Mozilla 于 2000 年在 Gecko 中实现了等效的 JavaScript 对象,Safari 和 Opera 在随后几年也采用了该机制,使其成为跨浏览器的能力。页面一旦能够在后台获取数据并只更新部分内容,Ajax 模式便切实可行,并支撑了单页应用的兴起。早期的 XHR 实现参差不齐,开发者常借助 jQuery 等库封装该 API,以屏蔽差异。XHR 依赖回调和手动状态跟踪,使较大的代码库难以维护。2015 年作为现代标准工作的一部分推出的 Fetch API,用 Promise 以及更简洁的 Request 和 Response 对象取代了这些模式。对于 404 等 HTTP 错误状态,Fetch 仍会正常完成,只有在网络故障时才会拒绝,这一行为常让初学者感到意外。两种机制默认都遵守同源规则。

浏览器之间的差异从未完全消失。各引擎实现各项 Web API 的时间并不一致,部分功能仍处于实验阶段、带有厂商前缀,或在某些设备上不可用。因此,开发者依赖特性检测(即在使用某个 API 前先检查其是否存在),以及用 JavaScript 补充缺失功能的填充库(polyfill)。转译新语法的工具可以解决语言本身的问题,但无法提供整个浏览器的接口,因此宿主环境的差异始终是 Web 开发中的常态。

服务器端与非浏览器运行时

在浏览器之外运行 JavaScript,最广为人知的方式是 Node.js。Ryan Dahl 对 Apache 等服务器处理大量同时连接的方式感到不满,于 2009 年 5 月 27 日发布了该项目。其第一个版本结合了 Google 的 V8 引擎、事件循环和底层 I/O 层,使得缓慢的磁盘或网络请求不会阻塞整个进程。最初的版本只能在 Linux 和 macOS 上运行,Windows 支持是之后才加入的。由于 Node.js 提供了文件系统、网络、子进程以及进程级的 API,为它编写的程序可以做到浏览器脚本无法完成的事情,例如读取本地文件、打开套接字以及管理自身的运行环境。该项目现由 OpenJS Foundation 以 MIT 许可证维护,其 API 也成为许多其他运行时和工具力求兼容的参照标准。有关该项目的历史和发布结构,参见 Node.js。

Node.js 并非唯一的运行时。Dahl 本人后来创建了 Deno,并于 2018 年宣布其为基于 V8 的 JavaScript 与 TypeScript 运行时。Deno 的设计旨在纠正他认为 Node.js 中存在的错误选择。它将 TypeScript 视为一等语言;除非程序获得明确授权,否则禁止访问文件、网络和环境变量;并且通过 URL 而非包管理器导入模块。后续版本增加了对 npm 包和 Node.js API 的兼容,模糊了一些原有的差异。Bun 于 2022 年首次发布,采取了另一条路线。它使用 Zig 编写,采用 Apple 的 JavaScriptCore 引擎而非 V8,并将打包器、测试运行器、包管理器和 SQLite 支持整合在单一可执行文件中。其开发者强调启动时间与吞吐量。已发布的基准测试结果因工作负载和作者而差异很大,因此对这些运行时的性能声明应结合具体使用场景加以验证。它们的安全模型也各不相同:Bun 默认不限制程序的权限,而 Deno 则会限制。

JavaScript 也通过应用框架进入桌面领域。Electron 是一个最初由 GitHub 开发的开源框架,它将 Chromium 渲染引擎与 Node.js 结合起来,使单一的 HTML、CSS 和 JavaScript 代码库能够生成适用于 Windows、macOS 和 Linux 的应用程序。Visual Studio Code、Slack 和 Discord 都是采用这种方式构建的广泛使用的程序。这种方法也有其代价:每个应用都会附带一份 Chromium 和 Node.js 的副本,因此下载体积更大,内存占用也比原生应用更高。移动端和跨平台框架遵循类似的思路。例如 React Native 允许开发者用 JavaScript 描述用户界面,再由框架将其映射为原生平台组件,因此该语言位于应用逻辑与操作系统控件之间,而并非取代它们。

在这些主要平台之外,JavaScript 也运行于嵌入式和命令行环境中。QuickJS 等轻量级引擎专为受限设备、插件系统以及需要在其他程序中嵌入脚本语言的工具而设计。命令行实用程序、构建工具和代码格式化工具通常运行于 Node.js 或同类运行时之上,许多云端和边缘平台则在隔离环境中执行 JavaScript 函数,这些环境启动迅速,且对宿主的访问受到限制。在每种情况下,语言核心(即其语法、类型和语义)基本保持不变,变化的是宿主提供的 API:服务器运行时提供文件和套接字访问,浏览器提供 DOM,而嵌入式设备则可能只提供由应用程序注入的少量函数。因此,代码在这些环境之间迁移时,实际的困难大多在于宿主 API、模块解析和打包方式,而非语言本身。

引擎、即时编译与垃圾回收

现代引擎并不会逐行读取 JavaScript 文本。V8(用于 Chrome 和 Node.js)、SpiderMonkey(Firefox)和 JavaScriptCore(Safari)等引擎会先将源代码解析为抽象语法树,再将其转换为一种紧凑的中间形式——字节码,解释器可以立即开始运行。三大主流引擎主要使用 C++ 编写,其中 SpiderMonkey 还使用了 Rust。

分层执行与即时编译

解释字节码的速度比直接运行原生机器码慢,因此引擎会监控哪些函数和循环被频繁执行,并把这些"热点"路径交给即时编译器,在运行过程中生成机器码。每个引擎都使用多个层级。V8 先使用 Ignition 解释器,然后将代码依次交给 Sparkplug 和 Maglev 编译器,最后进入负责深度优化的 TurboFan 编译器。SpiderMonkey 结合了基线解释器、基线 JIT 以及取代 IonMonkey 的优化层 WarpMonkey。JavaScriptCore 有四个层级:底层解释器、基线 JIT、DFG JIT 和 FTL JIT。这一设计是一种权衡:编译成本低的编译器能快速产出代码,而成本高的编译器则只用于那些运行时间足够长、值得投入的代码。

大部分优化都是推测性的。JavaScript 变量可以持有任意类型的值,对象的属性也可能随时增减,因此编译器会根据观察到的行为进行猜测,并生成针对这些猜测的专门化代码。如果之后某个操作打破了假设,引擎就会丢弃优化后的代码,回退到较慢的层级。这一过程称为去优化(deoptimization),它既保证了程序的正确性,又允许在常见情况下进行激进的优化。

隐藏类与内联缓存

尽管采用动态模型,有两种机制使属性访问依然很快。隐藏类(也称 shape)是引擎内部对对象布局的描述,记录存在哪些属性以及它们位于什么偏移位置。以相同方式创建的对象共享同一个隐藏类;添加属性时,对象会沿着转换链迁移到新的隐藏类。附着在每个属性访问位置上的内联缓存会记住它最近见到的隐藏类以及属性所在的位置。始终只见到一种形状的访问点称为单态(monomorphic),速度最快。多态(polymorphic)访问点会见到少量不同形状,而超多态(megamorphic)访问点则见到许多形状,只能退回到较慢的通用查找。这就是为什么按一致的顺序、使用一致的属性名构建对象的代码,在各大引擎中通常运行得更快。

垃圾回收

内存通过自动垃圾回收回收,程序无需显式释放对象。引擎通常采用分代回收器,其依据是大多数对象在创建后不久就会死亡。频繁运行的小型回收器只扫描较小的年轻代区域,而长期存活的对象会被提升到老年代区域,老年代的回收频率较低。开发者主要通过引用保留来影响内存使用:只要仍有可达的引用指向某个对象,它就会一直存活。因此,被遗忘的事件监听器、不断增长的缓存以及持有大量数据的闭包,都是常见的内存泄漏来源,尽管整个过程中并不涉及任何手动释放操作。

2008 年以来的性能

Chrome 于 2008 年发布,将 V8 带入公众视野,当时 JavaScript 主要是由简单解释器运行的页面脚本语言。此后,分层 JIT 编译、推测性优化以及基于隐藏类的属性访问,使所有主流引擎的速度都大幅提升。基准测试的方式也随之改变。早期的测量多为单一操作的微型测试,引擎可能以与真实应用无关的方式对其进行优化。如今的评估越来越关注启动时间、内存消耗和贴近实际的工作负载,因为这些更能反映用户的真实体验。

安全模型与常见漏洞

JavaScript 的安全问题主要源于网页的构建方式,而非语言的核心语义。浏览器会在同一页面中运行来自多个来源的脚本,因此主要关注点在于不可信数据能否改变受信任页面的行为。浏览器的安全模型通过源(origin)划分隔离边界,限制脚本可以访问的内容,并为请求设定规则,但应用代码仍可能突破这些边界。

与 JavaScript 相关的最常见网页漏洞是跨站脚本攻击(XSS)。在 XSS 中,攻击者使脚本进入受信任网站所提供的页面,从而让注入的代码以该网站的权限运行。它可以读取页面的 DOM、会话数据和表单内容,并以已登录用户的身份发送请求。XSS 通常源于对不可信输入的处理不当:服务器未经转义便将用户文本嵌入 HTML,或客户端代码通过 innerHTML 等属性将输入写入页面。反射型、存储型和基于 DOM 的 XSS 的区别在于恶意数据的传播路径不同,但它们的根源相同。

跨站请求伪造(CSRF)的原理有所不同。攻击者无法读取响应,而是诱使受害者的浏览器向另一网站发送会改变状态的请求,浏览器会自动附带该网站的 Cookie。防御手段包括服务器端校验的反 CSRF 令牌,以及 SameSite Cookie 属性,后者限制了跨站请求中何时发送 Cookie。原型污染则是 JavaScript 特有的风险。当合并或路径设置函数将攻击者控制的键(如 __proto__)复制到对象中时,攻击者便可以在 Object.prototype 上添加或修改属性,此后所有普通对象都会继承这些被注入的值。此类缺陷曾出现在广泛使用的工具库中,根据应用读取属性的方式,它可能导致 XSS、授权绕过或拒绝服务。

供应链攻击将威胁扩展到应用自身代码之外。现代项目会从 npm 注册表引入层级很深的依赖树,被入侵的软件包可能在安装过程中运行代码,或附带恶意逻辑并在生产环境中执行。2018 年一起广为人知的事件中,一个热门软件包的维护权被转交给了一名陌生人,此人加入了旨在窃取加密货币钱包的代码。由于同一运行时既执行应用又执行其依赖,单个恶意软件包即可触及应用能够触及的一切。锁文件、依赖审计以及审查新维护者是常见的缓解措施,但没有一种是完备的。npm 生态本身的情况见开源软件。

主要的防御手段是分层的。内容安全策略(CSP)通过 HTTP 响应头下发,允许网站声明哪些来源可以提供脚本,并可禁止内联脚本,除非它们带有 nonce 或哈希值。DOMPurify 等净化库会在内容插入页面之前清除危险标记。框架的默认转义机制(如 React 或 Vue 的默认模板行为)会自动编码数据,但显式的“逃生口”,如 React 的 dangerouslySetInnerHTML 或 Vue 的 v-html,一旦接收不可信内容,风险便会重新出现。当第三方脚本从 CDN 加载时,子资源完整性(SRI)可以提供帮助,因为浏览器会在执行文件前校验其哈希值。

有两种做法尤其值得警惕。在页面上下文中执行第三方代码,会让分析、广告或小部件供应商获得与宿主应用相同的访问权限,而供应商一旦被入侵,所有加载其代码的网站都可能受到影响。类 eval 的功能,包括 eval、new Function 以及传给 setTimeout 的字符串参数,会在运行时将文本当作代码编译。这使输入校验困难得多,也常与 CSP 相冲突,因此许多安全指南将其视为最后的手段。浏览器的隔离模型仍会限制被入侵页面能够触及的范围,但它无法保护网站免受自身代码缺陷的影响。

语言批评与设计怪癖

对 JavaScript 的批评通常集中在几个反复出现的方面。隐式类型转换是最常被提及的一点。只要 + 运算符的任一操作数是字符串,或是可以转换为字符串的对象,它就会执行拼接,因此两个空数组相加得到空字符串,数组与普通对象相加则得到文本 "[object Object]"。这些结果遵循已有文档记载的转换规则,但对初学者来说往往令人意外。其他常见的抱怨还包括:this 关键字的值取决于函数如何被调用,而不是它在源码中的位置;变量提升(hoisting)使 var 声明在其所在函数内全程可见,而在声明语句执行之前其值为 undefined;以及在非严格模式下,给未声明的名称赋值时,全局对象会获得新属性。

"WAT"一词源自 Gary Bernhardt 于 2012 年 1 月在 CodeMash 大会上所做的一场约四分钟的闪电演讲。演讲同时涉及 Ruby 和 JavaScript。他展示了一系列令人困惑的表达式,每个表达式后面都配有一张图片,图片上只有"WAT"这一个单词作为说明。这场演讲因此成为广为流传的程序员幽默。这个笑话的生命力已超出具体的示例本身,如今"WAT"被用来指代动态语言中任何出人意料的行为。

许多怪癖还与最初实现时的条件有关。Brendan Eich 于 1995 年用大约十天写出了最初的版本。批评者认为,这种赶工的速度,加上一个出于营销考虑而选定的名称,使得语言的设计从未得到全面的重新审视。最著名的例子是 typeof null 返回 "object"。在早期引擎中,值带有一个小型的类型标签,对象的标签为零,而 null 以空指针表示,空指针同样由全零位构成,因此引擎将两者混淆。当这一错误被意识到时,大量网页代码已经依赖于这一结果,修改它会导致这些页面失效。因此,它连同其他一些早期设计决定,仍保留在规范之中。

辩护者则回应说,只要掌握了语言的语义,它在内部就是自洽的。类型转换规则是确定的且有文档记载,因此那些"奇怪"的结果是可以预测的,而非随机的。他们还认为,许多臭名昭著的例子在现代代码中已很少出现。严格模式消除了意外产生的全局变量,并改变了普通函数调用中 this 的行为;let 和 const 取代了大部分 var 的用法;严格相等运算符 === 避免了大多数宽松比较带来的意外;代码检查工具(linter)以及 TypeScript 等静态类型检查器,则能在代码运行之前标记出许多有风险的写法。从这个角度看,刻意保持向后兼容是一种有代价的选择,而这些怪癖正是保持既有内容继续可用所付出的代价。

围绕该语言的若干方面,争论仍在继续。其一涉及标准库的规模与构成:多年来内置库较为单薄,开发者不得不依赖第三方包,而支持小型核心的人则认为,这样可以让平台保持灵活。其二涉及数字与日期。每个普通数字都是双精度浮点数,因此超过 2^53 的整数无法全部被精确表示,BigInt 为大整数解决了这一限制。Date 对象因其从零开始的月份索引、可变的实例,以及在不同引擎间表现不一的日期字符串解析而受到批评,Temporal API 的设计目的正是为了取代它。其三涉及动态类型系统本身。它有利于快速试验,但提供的编译期保证较少,这一权衡也推动了带类型超集的发展。这些问题至今仍无定论,社区一直在权衡它们,同时兼顾维护网络稳定的需要。

类型化超集、转译器与构建工具

大多数 JavaScript 代码并不是以编写时的原样发布的。源文件在到达浏览器或服务器之前,通常要经过一系列工具(类型检查器、编译器、打包器和包管理器)的处理,而这一工具链对日常开发实践的影响,如今已不亚于语言本身。

TypeScript 与类型化超集

TypeScript 由微软于 2012 年 10 月首次发布,开发工作由安德斯·海尔斯伯格(Anders Hejlsberg)领导。它为 JavaScript 增加了可选的静态类型。其编译器会检查这些类型注解,然后将其移除,因此输出的是任何引擎都能运行的普通 JavaScript。由于每个合法的 JavaScript 文件同时也是合法的 TypeScript 文件,代码库可以逐步迁移。类型检查发生在编译阶段,因此参数类型不匹配或属性名拼写错误会在程序运行前就被报告;类型推断则让开发者只需在有帮助的地方添加注解。“超集”一词本身也存在争议:一些批评者认为,这一说法只在较宽泛的意义上成立,因为 TypeScript 的类型系统并不属于 JavaScript 标准。由于输出的代码是普通 JavaScript,运行时速度通常相同,其代价主要体现在构建时间和额外的编译步骤上。

转译器与 polyfill

转译器(有时称为源到源编译器)在大致相同的抽象层次上,将一种版本的语言代码改写为另一种版本。Babel 是最知名的例子。它把较新的语法(如箭头函数、类和 JSX)转换为旧环境能够接受的形式,输出结果由目标浏览器或 Node.js 版本列表决定。不过,仅靠语法转换还不够。Promise 或 Array.from 等内置对象和方法由运行时提供,而不是由语法定义,因此转译器无法添加它们。Babel 可以自动注入由 core-js 提供的 polyfill 来填补这一空缺。因此分工很清楚:转译器负责改写语法,polyfill 库负责提供缺失的函数。旧的一体化包 @babel/polyfill 已被弃用,开发者现在应当分别引入 core-js 和 regenerator-runtime。

构建工具

生产环境的代码通常由一套职责各异的工具链组装而成。打包器将大量模块合并为少量文件以便交付。代码检查工具(linter)标记可疑或容易出错的代码,格式化工具统一代码风格,测试运行器则执行自动化检查。这些工具大多本身就是运行在 Node.js 上的 JavaScript 程序,其行为通常在包清单文件中声明,并配合各工具专用的配置文件。一些用更快的原生语言编写的新型编译器和打包器已开始取代这条流水线的部分环节,从而缩短了大型项目的构建时间。

npm 与依赖图

npm 是 Node.js 默认的包管理器。它由命令行客户端(同样称为 npm)和一个在线数据库组成,后者称为 npm 注册表(npm registry),收录公开包以及付费的私有包。其规模增长迅速,统计数字因日期和来源而异,近期数据显示包数量已超过 310 万。由于小型可复用包的发布和安装成本很低,单个应用往往依赖庞大的间接依赖树,这一点是现代 JavaScript 开发的显著特征。这种开放性也带来了一些后果。注册表对提交的包没有任何审核流程,因此依赖管理与维护、版本控制和安全问题密切相关。越来越多的项目依靠锁文件和自动化审计来控制其依赖树。

框架、库与 Web 应用架构

早期的库(如 jQuery)解决了一个实际问题:在浏览器对 DOM 和事件模型实现不一致的年代,同一段脚本在不同引擎中往往表现各异。jQuery 将元素选择、DOM 操作、事件绑定和 AJAX 调用封装进一套统一的 API 中,十多年间成为构建交互式页面的默认方式。它的思路是命令式的:开发者需要逐步描述页面的每一步变化,随着界面状态越来越复杂,这种方式也越来越难以维护。

大约从 2013 年起,基于组件的框架改变了这一模式。Facebook 发布的 React 要求开发者描述在给定状态下界面应呈现的样子,而状态之间的过渡由框架自行处理。它的实现依靠一种 虚拟 DOM:一棵轻量的、存在于内存中的 JavaScript 对象树。每次更新时,新旧两棵树会被比较(这一过程称为协调,reconciliation),只有差异部分才会写入真实的 DOM。Vue 于 2014 年首次发布,提供了一种渐进式的替代方案,采用模板语法;在后期版本中,它又引入了基于 JavaScript Proxy 构建的响应式系统,用以追踪界面各部分依赖的数据。Angular 则是从其前身 AngularJS 重写而来的完整框架,自带模板编译器、依赖注入以及面向大型应用的工具链。此后,这三个项目彼此之间的距离越来越近:Angular 引入了信号(signals)作为细粒度的响应式原语,Vue 的实验性 Vapor Mode 则将模板直接编译为 DOM 操作,不再经过虚拟 DOM。另有几个库,包括 Svelte 和 Solid,要么在编译阶段完成大部分工作,要么几乎不做运行时的差异比对。

这些框架中有许多是围绕单页应用设计的。在这种架构中,服务器只返回一个极简的外壳页面,浏览器则负责视图之间的路由切换,无需整页刷新。客户端路由将 URL 映射到组件,并通过 History API 更新地址栏。这种模式提升了交互响应速度,但也使首屏加载变慢,并增加了搜索引擎索引的难度,由此催生了服务端渲染。服务器为初始视图生成 HTML,随后客户端进行水合(hydration),即为已有的标记绑定事件处理器并恢复组件状态。Next.js、Nuxt 等元框架将这些模式连同路由、数据加载和构建配置一起打包为统一的结构。服务器组件及类似机制允许部分代码仅在服务器端运行,因此客户端与服务端代码之间的边界不再由文件或框架固定划分。

这些安排也带来了反复出现的工程权衡。在客户端渲染可以减轻服务器负担,并让交互变得迅速,但浏览器在页面可用之前,需要下载、解析并执行的脚本量也随之增加。服务端渲染降低了首屏的这部分开销,却增加了服务器负载,并要求代码在两种环境中运行。包体积、状态管理,以及集中式状态与局部状态之间的取舍,是每个主流框架社区都在争论的话题,各项目给出的答案反映了其各自的优先级。构建内容型网站、内部仪表盘和实时工具的团队,往往得出不同的结论。随着浏览器、构建工具以及语言本身的演进,整个生态也在不断修正其默认选择。

从 ES5 到 ECMAScript 2026的演进

ES5 于 2009 年定稿,增加了严格模式、属性描述符、原生 JSON 支持以及一组数组方法。这些改动使语言更加可靠,也填补了标准库中的空白。此后的版本在命名方式上发生了变化。发布版本不再以年份命名后,ES6 在正式用法中改称 ES2015,这一改动也表明该语言将持续演进。将 ES6 更名为 ES2015,是为了体现其发布年份,并摆脱版本号的命名方式。ES2015 是第一个通过 TC39 提案流程审议功能的版本,即采用基于提案的模式来讨论和采纳语言特性。它新增的内容包括类、Promise、箭头函数、ES 模块、生成器和迭代器,以及块级作用域的 let 和 const,还有 Map 和 Set 集合。其中,Promise 使异步编程成为可能,Map 和 Set 则是新的数据结构。

2015 年之后,委员会改为每年发布一个版本,因此每个版本所包含的已完成提案数量较少。后续版本引入了 async 函数、对象的剩余与展开属性、可选链、空值合并运算符、BigInt、私有类字段以及顶层 await。其中部分内容在有关语法和异步编程的小节中有概述。

ECMAScript 2026 是第 17 版。2026 年 6 月 30 日,欧洲计算机制造商协会(Ecma International)在日内瓦召开第 131 届大会,批准了 ECMA-262 第 17 版,即 ECMAScript® 2026 语言规范,标志着该版本最终定稿。正式批准主要是将确切的行为固定到规范文本中。许多新功能在批准之前就已在各个引擎中可用,因此 TypeScript 类型定义、Babel 和打包工具等工具链可以提前确定其支持情况,实现者与工具开发者也有了统一的规范依据。

库的新增内容大多规模较小,包括:Math.sumPrecise,用于对数量级不一的可迭代数字求和,并尽量减少精度损失;Iterator.concat,用于串联迭代器;Array.fromAsync,用于从异步可迭代对象及其他异步数据源构造数组;Error.isError,用于识别错误对象。此外还有 Map 和 WeakMap 的默认值查找、Uint8Array 的十六进制与 Base64 互转,以及新的 JSON 源码访问功能。Math.sumPrecise 针对的是所有普通数字所用浮点运算的一个众所周知的问题:依次累加十进制数时会损失精度。Error.isError 即使面对来自独立运行时沙箱的被捕获值,也能提供可靠的判断。并非所有亮点都是库函数:部分摘要提到的显式资源管理(Explicit Resource Management)新增了一对关键字,用于确定性的资源清理。

最受期待的新增内容是 Temporal,它是 Date 对象的替代品。Date 因混合了时间戳、本地时间、UTC 行为和可变状态,经常引发错误而遭到批评。Temporal 则提供了独立的、不可变的专用类型,如 PlainDate、ZonedDateTime、Instant 和 Duration,并原生支持 IANA 时区,其算术运算也能安全处理夏令时。多数资料认为 Temporal 进入最终 TC39 阶段的时间是 2026 年 3 月,即 Temporal 达到 TC39 第 4 阶段,并成为 ES2026 规范的一部分。另有一种说法将这一里程碑定在 2024 年末,并称浏览器随即开始发布支持,因此二手资料中的日期并不一致。浏览器支持也参差不齐:Firefox 于 2025 年 5 月 27 日发布的 Firefox 139 中默认启用了 Temporal,Chrome 于 2026 年 1 月 13 日发布的 Chrome 144 中加入了支持,而 Safari 尚未提供支持。因此,面向公众的项目应配合官方 polyfill 使用。

与其他领域和技术的关系

JavaScript与其他领域的联系分为两个方向。一些技术是为了在浏览器中与它协同工作而开发的,另一些则是建立在它之上、作为通用平台的技术。第一类中最直接的例子是WebAssembly。浏览器会与JavaScript一同执行它,它通常被用作C、C++、Rust和Go等语言的编译目标。它的渊源可以追溯到asm.js,这是JavaScript的一个严格子集,引擎可以对其进行激进的优化。由于asm.js仍需经过普通的JavaScript解析流程,编译速度较慢,于是出现了跳过文本解析的二进制格式作为回应。实践中,两者分工明确:WebAssembly负责视频解码、游戏和密码学等计算密集型的核心代码,JavaScript则负责用户界面和事件驱动的逻辑。两者之间的边界是有代价的。二者采用不同的内存模型,而且WebAssembly不能直接访问DOM,因此开发者需要通过JavaScript来完成DOM操作。

机器学习是第二个交叉领域。TensorFlow.js是一个开源库,让开发者可以在JavaScript中定义、训练和运行模型,无论是在浏览器中还是在Node.js下。它的API设计与Python版本的TensorFlow兼容,因此模型可以在两个生态之间迁移,项目还提供了用于转换Python构建模型的转换器。在浏览器中,它可以借助WebGL在GPU上进行计算。浏览器这种模式有其局限。小模型在客户端训练是可行的,但训练大模型比在原生硬件上慢得多,这种性能差距是服务器端或原生训练仍然普遍存在的主要原因之一。不过,在本地进行推理可以避免将用户数据发送到服务器,这也是开发者采用端侧模型的原因之一。

科学计算、数据可视化和教育领域采用JavaScript,很大程度上是因为它在浏览器中的地位。交互式笔记本环境让研究人员可以在无需本地安装的页面中,将文字说明、代码和实时输出结合在一起。图表和可视化库能把数据转化为可响应用户输入的图形,交互式学习材料也可以直接在读者的浏览器中运行代码示例。这些应用使JavaScript与数据科学以及更广泛的教育领域联系起来,其中包括通过网络教授编程的高等教育课程。其共同点在于可及性:浏览器标签页是大多数学习者和研究人员已经拥有的平台。

该语言的治理方式也使它与更广阔的计算机科学和开源软件世界联系在一起。规范是公开制定的。TC39委员会在GitHub上发布其工作成果,大约每两个月开会一次讨论提案。新特性会依次经过编号从0到4的阶段,委员会的网站指出,这一流程还包括其他中间步骤。进入最终阶段有明确要求,包括为共享测试套件编写验收测试,并需要两个兼容的实现通过这些测试。这使标准化成为学术式的规范工作与工业引擎开发之间的反馈循环。V8、SpiderMonkey和JavaScriptCore等引擎是由相互竞争的厂商维护的开源项目,它们必须就同一规范达成一致,同时在性能上彼此竞争。其结果是一种研究思路、实现经验与社区评审相互塑造的模式,这一模式也是该语言与其他开放标准所共有的。

普及程度、经济与劳动力市场

根据 2026 年版 Stack Overflow 开发者调查,在使用最广泛的编程语言中,JavaScript 已连续第 12 年位居榜首。具体占比取决于统计的受访者范围。对 2025 年调查的一项概括显示,在全体开发者中,有 66% 将其列为第一,而在职业开发者子群体中,这一比例达到 68.9%。使用率调查与其他流行度指标也有所不同:TIOBE 指数统计的是搜索结果数量,而非开发者人数,两个指数目前仍将 Python 排在首位。在 GitHub 上,自 2025 年起,按贡献者人数计,TypeScript 已超过 JavaScript。

招聘数据也呈现相似的格局。CoderPad 2025 年报告中的招聘者数据显示,JavaScript 是需求第二高的编程语言,有 41.5% 的招聘者正在积极招募 JavaScript 开发者,仅次于 Python。2026 年 6 月对某招聘网站上 6138 条前端职位的分析显示,48.7% 的职位要求提及 JavaScript,但没有任何单一技能超过一半。同一数据集显示,明确要求 TypeScript 的美国职位薪资中位数为 147200 美元,而仅列出普通 JavaScript 的职位为 120000 美元,这表明这一带类型的超集如今占据了大部分薪资溢价。

不同统计机构给出的数字差异很大。一项指数显示,2026 年 8 月有 542 个在招职位,在所追踪的 16 种语言中 JavaScript 排名第 5;另一项统计则在某一时点列出 273 个在招职位。这些数字采用的方法不同,无法直接比较。对于这一趋势,业界看法也并不一致。Upwork 的评估认为,JavaScript 的需求比以往任何时候都更旺盛,但使用它的方式(以及成功运用它所需的技能)已经发生了彻底的变化。另有行业报道指出,随着 AI 工具承担起常规编码工作,初级开发者的需求有所减弱,入行比 2021 年更加困难。

从经济角度看,JavaScript 是商业网络的重要底层支撑。商品页面、结账流程、个性化推荐和广告脚本都在浏览器中运行,许多软件即服务产品也以依赖它的单页应用形式构建。借助 Node.js,同一种语言既能服务客户端,也能服务服务器端,减少了团队所需掌握的不同技能种类。这也是为什么尽管实际工作正越来越多地转向 TypeScript、框架和工具链,JavaScript 仍频繁出现在众多电子商务和软件工程的职位描述中。

企业赞助从多个方向影响着这门语言。浏览器厂商资助着执行它的引擎:Google 的 V8 为 Chrome 和 Node.js 提供动力,Apple 开发了用于 Safari 的 JavaScriptCore,Mozilla 则维护着用于 Firefox 的 SpiderMonkey。大型企业还推动了主要框架和工具的发展:React 诞生于 Meta,Angular 来自 Google,TypeScript 来自微软,Next.js 由 Vercel 维护。这些工作大多以开源软件的形式在各基金会下发布,但商业利益仍会影响路线图,而且浏览器厂商和大型公司的代表也参与了定义该语言的标准委员会。

对脚本的依赖也引发了无障碍访问方面的问题。只有在脚本执行后才能渲染的页面,在较旧或性能较低的智能手机上、在网络较慢的环境中,或脚本被禁用的情况下,都可能无法正常显示。如果开发者不认真构建,自定义界面组件可能无法被辅助技术使用。渐进增强、服务器端渲染和常规的无障碍测试是常见的应对方式。这些问题将该语言的商业成功与数字鸿沟联系在一起,因为如今访问许多在线服务都默认用户使用支持现代 JavaScript 的浏览器和性能尚可的设备。

参考来源

  1. History of JavaScript - read our article to find out!softteco.com
  2. Brendan Eichen.wikipedia.org
  3. Brendan Eich: JavaScript, the Language That Made the Web Interactiveblog.immunitynetworks.com
  4. Brendan Eichebsco.com
  5. A Brief History of JavaScript: Everything You Need to Know in 2024nestify.io
  6. DEV Communitydev.to
  7. ECMAScript version historyen.wikipedia.org
  8. The TC39 Processtc39.es
  9. ECMAScript Language (ECMA-262), including JavaScriptloc.gov
  10. ECMAScript version history explainedeverything.explained.today
  11. History of ECMAScriptbenmvp.com
  12. JavaScript data types and data structures - JavaScriptdeveloper.mozilla.org
  13. JavaScript Data Typesjavascripttutorial.net
  14. Primitive Types - 33 JavaScript Concepts33jsconcepts.com
  15. JavaScript Data Types: Primitive Types and Objectstutorialreference.com
  16. this - JavaScriptdeveloper.mozilla.org
  17. Closures & Lexical Scope: How JavaScript Functions "Remember" Where They Were Born - DEV Communitydev.to
  18. javascript fundamentals 92bjavascriptcorner.substack.com
  19. Event loop: microtasks and macrotasksjavascript.info
  20. JavaScript Event Loop Explained: Microtasks & Macrotasks - DEV Communitydev.to
  21. The JavaScript Event Loop: Macrotasks vs Microtasks - AlexWebLab in Bangkok, Thailand now, before in Hong Kong 香港alexweblab.com
  22. JavaScript Event Loop Explained (With Examples) 2026crosscheck.cloud
  23. Understanding the Event Loop in JavaScript/TypeScript - NashTech Blogblog.nashtechglobal.com
  24. Exploring Node.js Modules: CommonJS vs. ES6 Modulesmedium.com
  25. CommonJS vs. ES Modulesbetterstack.com
  26. Comparing ECMAScript Modules and CommonJSwanago.io
  27. ES Modules and CommonJS: An Overview - DEV Communitydev.to
  28. which one is better commonjs or ecma script syntax for a nodejs application b80e935f6b55zainsadaqat.medium.com
  29. Same-origin policyen.wikipedia.org
  30. Same-origin policy (SOP)portswigger.net
  31. Cross-Origin Resource Sharing (CORS) - HTTPdeveloper.mozilla.org
  32. Understanding CORS and Same Origin Policy in Web Security🚨 - DEV Communitydev.to
  33. The history of XMLHttpRequest (XHR)medium.com
  34. DEV Communitydev.to
  35. The Fetch API is finally stable in Node.js - LogRocket Blogblog.logrocket.com
  36. Javascript Fetch API: The XMLHttpRequest evolution - Developer How-todeveloperhowto.com
  37. XMLHttpRequest (XHR)developer.mozilla.org
  38. Node.jsen.wikipedia.org
  39. Ryan Dahlen.wikipedia.org
  40. Node.js Introduced at JSConf.eu (2009)centerconsulting.com
  41. Brief history and evolution of Node.jsluisllamas.es
  42. Node.js — Historynodejs.ximg.app
  43. Node.js vs Deno vs Bun: Comparing JavaScript Runtimesbetterstack.com
  44. Deno 2 vs Bun: which JavaScript runtime is right for you? · Appwriteappwrite.io
  45. Node.js vs Bun vs Deno: Best JavaScript Runtime in 2025sevensquaretech.com
  46. Deno vs Bun: Comparing Modern JavaScript Runtimesdev.to
  47. GitHub - electron/electron: :electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS · GitHubgithub.com
  48. Electron App Development: The Complete 2026 Guidecoderio.com
  49. Electron Software Framework: The Best Way to Build Desktop Apps?pangea.ai
  50. Build cross-platform desktop apps with JavaScript, HTML, and CSSelectronjs.org
  51. V8 vs SpiderMonkey vs JavaScriptCorelanguagelineage.org
  52. Meet the JavaScript Engines Powering the Webblog.openreplay.com
  53. NOJITSU: Locking Down JavaScript Enginesndss-symposium.org
  54. Two-level Just-in-Time Compilation with One Interpreter and One Enginearxiv.org
  55. JavaScript Engines Explained—Comparing V8, SpiderMonkey, JavaScriptCore, and More · Frontend Dogmafrontenddogma.com
  56. Hidden V8 optimizations : hidden classes and inline cachingmedium.com
  57. Fast Property Accesschromium.googlesource.com
  58. JavaScript Engines - 33 JavaScript Concepts33jsconcepts.com
  59. What is JavaScript written in? V8, SpiderMonkey, and JSClanguagelineage.org
  60. Analyzing the JavaScript Examples in Gary Bernhardt's "Wat" Talk — Marius Schulzmariusschulz.com
  61. JavaScript Says WAT!yassinemoumen.com
  62. Gary Bernhardt - De Programmatica Ipsumdeprogrammaticaipsum.com
  63. typeof null is "object" — a bug from 1995 nobody can fix - DEV Communitydev.to
  64. typeof null: investigating a classic JavaScript bug · Caffeinspirationalexanderell.is
  65. History behind typeof null equals object.webdioxide.com
  66. TypeScript vs JavaScript in 2026sencha.com
  67. Babel (transcompiler)en.wikipedia.org
  68. npmen.wikipedia.org
  69. TypeScript Overviewtutorialsteacher.com
  70. Types without TypeScriptdepth-first.com
  71. What is Babel Transpiler?educative.io
  72. core-js/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md at master · zloirock/core-jsgithub.com
  73. Reactivity Models Compared: React, Vue, Angular, Svelteblog.openreplay.com
  74. Virtual DOMen.wikipedia.org
  75. React vs Angular vs Vue (2026): Which to Pick, With Datahashbyt.com
  76. The Reactivity Wars: Virtual DOM, Signals, and the React Compiler in 2026daily.dev
  77. ES6 (ES2015) and Beyond: Understanding JavaScript Versioning — SitePointsitepoint.com
  78. What is ES6 in JavaScript? A Comprehensive Guide to ECMAScript 2015talent500.com
  79. Ecma International approves new standards - Ecma Internationalecma-international.org
  80. ECMAScript 2026 Is Official: Everything New in JavaScript's 17th Editionnatanaelrusli.com
  81. ECMAScript 2026 specification approvedinfoworld.com
  82. ECMA 2026 Spec Approvedi-programmer.info
  83. JavaScript 2026w3schools.com
  84. JavaScript Temporal API: Replacing Date, With Exampleskvassiliou.com
  85. JavaScript Temporal API: The Modern Replacement for Datematthewswong.com
  86. Temporal API: The Modern Replacement for JavaScript Datethingsaboutweb.dev
  87. JavaScript's Date Problem Is Finally Fixed: The Temporal API After 9 Years · Groundygroundy.com
  88. The Temporal API Is Here. Here's How to Actually Start Using Itdainemawer.com
  89. WebAssemblytechtarget.com
  90. Next-generation Web Applications with WebAssembly and TruffleWasmarxiv.org
  91. WebAssembly enables low latency interoperable augmented and virtual reality softwarearxiv.org
  92. WebAssembly: a binary format for the web2ality.com
  93. TENSORFLOW.JS: MACHINE LEARNING FOR THE WEB AND BEYONDproceedings.mlsys.org
  94. TensorFlow.jstensorflow.org
  95. TensorFlow.js: Deploying and Training Machine Learning Models Right in a Browseraltoros.com
  96. TC39 - Specifying JavaScript.tc39.es
  97. TC39 Standards Processdeepwiki.com
  98. Technology 2026survey.stackoverflow.co
  99. Top Programming Languages 2026: Developer Usage Rankings And Datacommandlinux.com
  100. Top 15 Programming Languages by Usage Statistics [2026] - Second Talentsecondtalent.com
  101. Is JavaScript Still in Demand in 2026? - Upworkupwork.com
  102. Frontend Developer Skills in 2026: 1 in 5 Openings Is Entry-Levelmedium.com
  103. JavaScript Developer Jobs — Demand, Roles and Skills in 2026datamatastudios.com
  104. JavaScript Developer Demand 2026 — Is It Worth Learning?devskopehq.com
  105. Is JavaScript Still Worth Learning in 2026? An Honest Look at the Job Marketuselearnai.com