ES6 是 ES2015 的旧称。「ES6+」通常泛指 ES2015 及后续版本;它们增加了语法和内置对象, 但没有替换 JavaScript 的核心模型。
简洁语法可能藏起原有语义:const 对象仍可修改,展开只做浅拷贝,可选链只保护明确写出的链,
Promise 也不会自行取消。
根据语义选择特性,测试 null、0 和共享嵌套对象等边界值,并分别验证解析器支持与所需的
运行时 API。
是什么,为什么存在
ES6 是第六版 ECMAScript 的非正式名称。它在 2015 年标准化,
所以正式名称是 ES2015。
ECMAScript 版本(ECMAScript edition) 定义 JavaScript 语言;
浏览器和 Node 等运行时实现这份规范。「ES6+」没有精确的上限,通常包含 ES2015 以及后续每年加入的
async 函数、对象展开、可选链和空值合并等特性。
重点不在于背一张新标点清单。现代特性为原有 JavaScript 语义
提供了更清楚的写法:块级绑定减少
var 引起的意外交互;解构为值的各部分命名;迭代协议让不同集合共享遍历契约;Promise 则把
异步工作的结果变成可组合的值。底层规则符合任务时,这些特性可以省去粘合代码。
当前应用代码、软件包源码、构建配置和生成代码中都会出现这层语言特性。 即使项目最终发布的是 转换后的产物,源码通常仍采用现代语法。目标运行时依然重要,因为解析器、语法转换、内置对象与 宿主 API 是四道不同的兼容性边界。
这个主题梳理这些边界以及跨越边界的常用特性。解构、集合、类、模块和异步控制流分别有专门主题 展开讲解。阅读本篇是为了看清它们怎样配合,以及哪些假设必须通过测试验证。
工作原理
现代 JavaScript 特性大致分为两类。语法变更影响源码的解析方式,
例如 const、箭头函数、解构模式、
可选链和 import 声明。运行时新增内容则是程序调用的对象或方法,例如 Map、Set、Promise 和
较新的集合方法。有些提案会同时增加语法与运行时行为。
let 和 const 创建词法绑定,作用域是最近的块。进入该块后、声明完成初始化前,两者都处于
暂时性死区。const 禁止给绑定重新赋值,但不会冻结通过该绑定访问的对象。绑定应保持同一标识时
使用 const;算法本来就需要重新赋值时使用 let。
箭头函数缩短了函数表达式,并从外层词法环境捕获 this。
它没有自己的 this、arguments,也不能
作为构造函数。这使它很适合小型回调,却不适合机械地替换对象方法;对象方法的接收者可能需要由
调用位置决定。
解构是一种模式,不是通用的深拷贝操作。数组模式按顺序消费 可迭代对象
(iterable) ,对象模式则读取具名属性。默认值只在取出的值为 undefined 时执行。剩余语法
收集余下输入,展开语法则把输入展开到函数调用、数组字面量或对象字面量中。
数组展开遵循迭代协议。对象展开的规则不同,它把自有的可枚举字符串属性和 Symbol 属性复制到
新的普通对象。在两种常见容器中,结果都是 浅拷贝(shallow copy) :
嵌套对象的引用仍然共享。属性顺序也很重要,因为后出现的对象展开或属性定义会覆盖先出现的值。
当左侧值为 null 或 undefined 时,可选链使用 短路求值
(short-circuit evaluation) 。空值合并表达式 left ?? fallback 也只为这两个值选择回退。
left || fallback 则不同,它还会替换 0、false、空字符串和 NaN。
Map、Set 与迭代协议明确了集合行为。
Map 可以用任意类型的值作为键,并在迭代时保持插入顺序。
Set 按照 SameValueZero 相等规则为每个不同值保留一个条目。生成器通过包含 yield 的函数管理
迭代器状态,因此使用方可以通过 for...of 或展开语法逐项取得值。
Promise 表示最终完成的结果。async 函数总是返回 Promise;await 会暂停该函数,直到等待的值落定,
但不会阻塞 JavaScript 线程。Promise.all 等组合器表达一组操作的处理策略。ECMAScript 模块通过
明确的 import 和 export 建立关系,模块加载与软件包解析则由宿主运行时提供。
实际的兼容性检查分为四步:
- 解析器必须能识别源码语法。
- 语法转换必须保留代码依赖的语义。
- 运行时必须提供所需的内置对象和方法。
- 宿主必须提供 DOM 或 Node 文件系统模块等平台 API。
构建成功只能证明该构建实际执行过的检查。它不能证明目标运行时具备每个内置对象,也不能证明 浏览器提供 Node API,或转换后的代码保留迭代器关闭、属性访问顺序等可观察细节。
示例
词法绑定与简洁函数
这个示例组合了块级绑定、箭头函数、参数解构、默认参数、模板字符串和 for...of。每项特性都省掉
少量样板,但数据流仍然清楚:taxRate 是不会重新赋值的绑定,lowStockCount 则会重新赋值。
const products = [
{ sku: 'A-10', price: 12, stock: 2 },
{ sku: 'B-20', price: 8, stock: 0 },
];
const taxRate = 0.2;
const labelProduct = ({ sku, price, stock = 0 }, currency = 'EUR') => {
const gross = price * (1 + taxRate);
return `${sku}: ${gross.toFixed(2)} ${currency}, stock=${stock}`;
};
let lowStockCount = 0;
for (const product of products) {
console.log(labelProduct(product));
if (product.stock < 1) lowStockCount += 1;
}
console.log(`low stock: ${lowStockCount}`);A-10: 14.40 EUR, stock=2
B-20: 9.60 EUR, stock=0
low stock: 1参数模式说明了格式化函数会读取哪些商品字段。stock = 0 默认值适用于缺失属性或显式的
undefined,但不适用于 null。循环中的 product 使用 const,因为每轮迭代都会创建一个绑定,
而循环体不会给它重新赋值。
规范化记录而不抹掉合法假值
边界代码经常需要分层设置默认值。解构处理缺失字段,可选链处理缺失的中间对象,?? 保留合法的
零值,对象展开让用户偏好覆盖默认偏好。函数返回新结果,不会修改输入。
function normalizeCustomer(raw = {}) {
const {
id = 'guest',
contact,
preferences = {},
retries = 3,
} = raw;
return {
id,
email: contact?.email ?? 'missing',
preferences: { theme: 'light', ...preferences },
retries,
};
}
const customer = normalizeCustomer({
id: 'U-7',
contact: null,
preferences: { density: 'compact' },
retries: 0,
});
console.log(JSON.stringify(customer));{"id":"U-7","email":"missing","preferences":{"theme":"light","density":"compact"},"retries":0}显式的 null 联系信息会在 contact?.email 处停止,因此表达式得到 undefined,再由 ?? 提供
missing。代码没有使用 ||,所以重试次数 0 得以保留。嵌套的偏好对象也是新对象,但其中若存放
其他对象,这些对象仍会共享。
用集合协议为事件建立索引
Map 表示以用户 ID 为键的索引,每个 Set 则去掉同一用户的重复操作。生成器按需提供摘要。最后的
展开会把生成器消费成数组,这说明内置集合与自定义生成器采用相同的迭代协议。
const events = [
['U-1', 'view'],
['U-2', 'buy'],
['U-1', 'buy'],
['U-1', 'view'],
];
const actionsByUser = new Map();
for (const [userId, action] of events) {
const actions = actionsByUser.get(userId) ?? new Set();
actions.add(action);
actionsByUser.set(userId, actions);
}
function* summarize(index) {
for (const [userId, actions] of index) {
yield `${userId}:${[...actions].join(',')}`;
}
}
console.log([...summarize(actionsByUser)].join(' | '));U-1:view,buy | U-2:buy输出遵循 Map 键与 Set 值的首次插入顺序。再次为 U-1 添加 view 不会产生第二个 Set 条目,也不会
移动原条目。需要排序时应明确执行排序,而不是假设另一种集合类型会给出所需次序。
组合相互独立的异步工作
两次等待相互独立,因此 Promise.all 会在使用任一结果前观察两者。尽管商品 Promise 先落定,
async 函数恢复时拿到的结果数组仍按输入顺序排列。.mjs 扩展名告诉 Node 24 把这个自包含文件
视为模块,因此文件可以使用顶层 await。
const wait = (value, milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds, value));
async function loadOrderSummary() {
const [customer, items] = await Promise.all([
wait({ name: 'Mira' }, 20),
wait(
[
{ quantity: 2, unitPrice: 6.25 },
{ quantity: 1, unitPrice: 5 },
],
5,
),
]);
const total = items.reduce(
(sum, { quantity, unitPrice }) => sum + quantity * unitPrice,
0,
);
return `${customer.name}: ${items.length} items, EUR ${total.toFixed(2)}`;
}
console.log(await loadOrderSummary());Mira: 2 items, EUR 17.50Promise.all 会让结果位置与输入位置保持对应,落定顺序不会改变结果次序。任一输入拒绝时,组合后的
Promise 也会拒绝。除非底层操作的 API 支持取消,而且代码明确提出取消请求,否则其他操作仍会继续。
陷阱
修复方法: 使用 const 表达绑定意图,再单独选择数据所有权策略。调用方需要快照时构造新对象;
状态本来就要变化时,通过范围明确的所有者公开修改,并测试嵌套引用。Object.freeze() 也是浅层操作,
不能代替对嵌套值所有权的分析。
修复方法: 只处理 undefined 时使用解构默认值,null 与 undefined 都表示缺失时使用 ??。
如果 null 表示格式错误而非缺失,应单独验证。不要只测试缺失属性,还要为领域允许的每个假值添加测试。
修复方法: 按明确的允许列表构造公开输出。在所有权必须改变的层级复制领域值,也不要假设展开会 保留原型、访问器或属性描述符。修改结果中的嵌套值,并向输入新增意外凭据,以这两种测试检查边界。
修复方法: 标出每个确实可选的跳转,例如 account?.profile?.name,并决定缺失值应在哪一层变成
错误或回退。不要到处追加 ?.;必需字段仍应在验证时失败,不能悄悄传播 undefined。
修复方法: 对支持协作式取消的操作传入 AbortSignal,或者等必要的清理完成后再释放共享资源。
只有调用方确实需要每项结果时才选择 Promise.allSettled;它改变的是报告策略,不是取消行为。
版本、转换与运行时支持
ECMAScript 2015 的改动范围很大,因此「ES6」这个简称一直沿用。此后的规范按年份发布。当前运行时
不会开启一个整体的「ES6 模式」,而是实现来自多个版本的一组语法与运行时语义。本主题 frontmatter 中的
Node 24 记录了可运行示例使用的环境,并不表示每个浏览器都具有相同支持。
版本名称回答的是某项语言特性在哪里标准化,却不能单独回答项目能否使用它。这个决定还取决于目标引擎、 文件的解析目标、构建转换和可用全局对象。即使 JavaScript token 本身有效,软件包元数据和文件扩展名 也可能改变 Node 把文件解析为 ECMAScript 模块还是 CommonJS。
解析发生在特性检测之前
不支持的语法通常会在解析时失败。如果引擎无法解析同一文件其他位置的可选链,就没有机会运行
if (supportsOptionalChaining) 之类的条件。构建工具可以先降低语法再交给目标,也可以向该目标发布
另一份文件。运行时特性检测只适用于引擎已经能够解析的值。
内置对象的失败方式不同。即使某个引擎没有 Map,包含 new Map() 的源码也可能通过解析,随后在
执行该行时抛出引用错误。语法转换器不一定会安装全局对象或方法。项目如果使用 polyfill,就必须让它
符合目标环境、加载顺序和应用所需语义。
简洁语法仍有可观察操作
源码更短不代表语义步骤更少。对象解构会读取属性,可能调用 getter 或 Proxy trap。数组解构会请求 迭代器值,并可能在模式提前结束时关闭迭代器。对象展开会按规定顺序枚举符合条件的键并读取其值。 之后发生的失败不会回滚已经产生的效果。
这在信任边界尤其重要。解构本身不验证类型,可选链无法区分有意缺失的字段与格式错误的数据,对象展开 也不会定义输出 schema。应把这些形式视为访问和构造语法,另行验证外部值,并单独列出公开字段。
模块会改变加载契约
ECMAScript 模块拥有自己的顶层作用域,以严格模式运行,并且只通过明确的 export 暴露绑定。静态
import 声明作为模块加载的一部分解析,不是普通函数调用。顶层 await 属于模块语法,所以宿主必须
先把文件识别为模块。
ECMAScript 规范定义模块记录和语言行为,Node 与浏览器则定义如何通过说明符定位资源。在打包工具中 能够解析的路径,直接交给 Node 时未必能解析。应验证实际部署入口、软件包元数据和 import 说明符, 不要根据编辑器的语法高亮推断模块行为。
兼容性声明需要可执行证据
有效的兼容性测试应该运行目标实际收到的产物。对于未经转换的 Node 代码,可以执行 node entry.mjs。
对于打包后的浏览器应用,应使用生产目标配置构建,并在支持范围内最旧的引擎中打开产物。对源码文件运行
单元测试可以覆盖应用行为,却不一定覆盖部署时使用的解析器和加载器。
在兼容性声明旁记录目标和构建路径。「现代 JavaScript」对于发布决策过于宽泛;「未打包的 .mjs
入口可以在 Node 24 运行」则可测试。支持情况改变后,应重复同一命令或浏览器测试,而不是猜测上一位作者
所说的「ES6+」究竟指什么。
延伸阅读
4个问题 · 1 道输出预测题 · 1 道找错题