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道找错题