很多人把 let、箭头函数和 Promise 称为"ES6",又把可选链、async/await 甚至所有现代 JavaScript 都笼统地叫作 ES6。这样交流虽然方便,却容易混淆特性所属版本、运行环境支持程度和构建工具职责。
这篇文章从 ECMAScript 2015,也就是通常所说的 ES6 开始,一直讲到 ECMAScript 2026。每个重要特性都会回答五个问题:
- 它解决了什么问题?
- 基本语法怎么写?
- 运行规则是什么?
- 工作中落在哪里?
- 有哪些兼容性与使用误区?
文章只把已经进入正式年度标准的能力放进 ES2015 至 ES2026 主线。仍在 TC39 提案阶段的语法会放在最后单独说明,避免把 Babel 实验插件、框架语法和 ECMAScript 标准混为一谈。
一、开始之前:ES6 到底是什么
1. ECMAScript、JavaScript 与宿主环境
ECMAScript 是语言规范,定义语法、类型、对象、模块、Promise 等语言规则。JavaScript 是 ECMAScript 最广泛的实现和使用名称。
浏览器还提供 ECMAScript 规范之外的宿主 API:
text
ECMAScript:Array、Map、Promise、Proxy、import、class
Web API:DOM、fetch、setTimeout、localStorage、Web Worker
Node.js API:fs、http、process、Buffer
因此 Promise 是 ECMAScript 特性,fetch 不是;async/await 是语言语法,网络请求是否可以取消取决于 fetch 和 AbortController 等宿主能力。
2. 为什么 ES6 又叫 ES2015
ECMAScript 第六版在 2015 年发布,所以同时被称为 ES6 和 ES2015。此后规范采用年度发布节奏:ES2016、ES2017,一直到 2026 年 6 月正式发布的 ES2026。
ES2015 是一次规模很大的升级,加入块级作用域、箭头函数、类、模块、Promise、迭代器、生成器、Map、Set、Proxy 等大量能力。后续版本通常更聚焦,每年吸收一批已经成熟的提案。
3. TC39 提案阶段意味着什么
新语言特性通常经历以下阶段:
text
Stage 0:想法
Stage 1:委员会愿意讨论问题和方向
Stage 2:核心设计已经形成,但仍可能明显变化
Stage 2.7:规范文本完成,等待验证与测试
Stage 3:规范基本稳定,等待实现反馈
Stage 4:完成,具备进入正式标准的条件
工程判断不能只看"Chrome 能运行":浏览器可能提前实验实现,Babel 也可能提供转换插件。生产代码应综合确认特性是否 Stage 4、目标浏览器和 Node.js 是否支持、构建工具能否转换、是否还需要运行时 polyfill。
4. 转译和 polyfill 不是一回事
转译负责把新语法改写为旧语法,例如把箭头函数改成普通函数;polyfill 负责补充旧环境不存在的运行时 API,例如 Array.prototype.includes。
js
// 新语法:可由编译工具转换
const add = (a, b) => a + b
// 新 API:旧环境需要 polyfill,单纯改写语法无法创造这个方法
[1, 2, 3].includes(2)
Proxy 这类底层语义很难完整 polyfill。兼容旧环境时,必须按具体特性分析,不能认为"用了 Babel 就全部兼容"。
二、ES2015(ES6):现代 JavaScript 的基础
2.1 let、const 与块级作用域
概念
var 只有函数作用域,并允许重复声明;let 和 const 引入块级作用域。块包括函数体、if、for、while 以及单独的 {}。
js
if (true) {
let message = 'inside'
const version = 2015
}
// console.log(message) // ReferenceError
const 表示绑定不能重新指向另一个值,不表示对象内部不可修改:
js
const user = { name: 'Alice' }
user.name = 'Bob' // 合法
// user = {} // TypeError
暂时性死区
let 和 const 的绑定在进入作用域时已经创建,但执行到声明语句前没有初始化,这段区域叫暂时性死区(TDZ):
js
{
// console.log(count) // ReferenceError
let count = 1
}
不要简单记成"let 不提升"。更准确的是:绑定已创建,但初始化前不能读取。
循环中的独立绑定
let 在 for 循环中会为每轮迭代创建独立绑定:
js
for (let index = 0; index < 3; index += 1) {
setTimeout(() => console.log(index), 0)
}
// 0 1 2
如果使用 var,三个回调共享同一个绑定,执行时循环已经结束,通常输出 3 3 3。
工作落点
- 默认使用
const,只有绑定确实需要改变时使用let。 const能表达"这个变量不会被重新赋值",降低阅读时的状态搜索范围。- 不要为了追求
const把简单流程改造成难懂代码;局部计数器和状态机使用let很正常。 - 需要不可变对象时,使用不可变更新、
Object.freeze或相应状态管理约束;const本身不负责深层不可变。
2.2 箭头函数
基本语法
js
const add = (a, b) => a + b
const normalizeUser = (user) => ({
id: String(user.id),
name: user.name.trim(),
})
返回对象字面量时要加括号,否则 {} 会被解析成函数体。
箭头函数没有自己的 this
箭头函数从定义位置捕获外层 this,不会因为 call、apply 或对象方法调用而改变:
js
class Timer {
seconds = 0
start() {
this.id = setInterval(() => {
this.seconds += 1
}, 1000)
}
}
这里使用箭头函数很合适,因为回调需要继续使用 start 方法中的实例 this。
但对象方法需要动态接收调用者时,不应该使用箭头函数:
js
const user = {
name: 'Alice',
show: () => console.log(this.name),
}
user.show() // 通常不是 Alice
箭头函数还没有自己的 arguments、super 和 new.target,不能作为构造函数,也没有可供 new 使用的 prototype。
工作落点
- 数组转换和简短回调:
items.map((item) => item.id)。 - 需要捕获外层
this的异步回调。 - 不适合构造函数、依赖动态
this的方法,以及需要清晰函数名和复杂控制流的大函数。 - 箭头函数不是普通函数的完全替代品,选择依据是语义,不是字符更少。
2.3 参数默认值、剩余参数与展开语法
默认参数
默认值只在参数为 undefined 或未传时生效,传入 null 不会触发:
js
function createRequest(url, timeout = 5000) {
return { url, timeout }
}
createRequest('/users') // timeout: 5000
createRequest('/users', undefined) // timeout: 5000
createRequest('/users', null) // timeout: null
默认参数可以引用前面的参数:
js
function createRange(start, end = start + 10) {
return { start, end }
}
对象配置参数通常配合解构和整体默认值:
js
function request(url, { timeout = 5000, retry = 0 } = {}) {
// ...
}
request('/api/users')
末尾的 = {} 很重要,否则第二个实参缺失时,解构 undefined 会抛错。
剩余参数
剩余参数把多余实参收集成真正数组:
js
function sum(first, ...rest) {
return rest.reduce((total, value) => total + value, first)
}
sum(1, 2, 3) // 6
剩余参数必须位于最后,一个函数只能有一个剩余参数。它比 arguments 更明确,也可以直接调用数组方法。
展开语法
展开语法把可迭代对象展开到参数或数组中:
js
const base = [1, 2]
const result = [0, ...base, 3]
Math.max(...result) // 3
ES2015 的数组展开要求值可迭代。对象展开是 ES2018 才正式加入的特性。
展开不是深拷贝:
js
const source = [{ id: 1 }]
const copied = [...source]
copied[0].id = 2
console.log(source[0].id) // 2
工作中常用于合并参数、构造新数组和不可变更新第一层。超大数组不要直接 fn(...items),因为引擎对实参数量存在限制。
2.4 解构赋值
数组解构按位置
js
const [first, second = 'default', ...rest] = ['A']
console.log(first) // A
console.log(second) // default
console.log(rest) // []
可以跳过不需要的位置,也可以交换变量:
js
const [, secondItem] = items
let left = 1
let right = 2
;[left, right] = [right, left]
上一行如果可能与 (、[、模板字符串等连在一起,需要关注自动分号插入,示例前的分号用于明确切断上一条语句。
对象解构按属性名
js
const user = { id: 1, profile: { nickname: 'Alice' } }
const {
id: userId,
profile: { nickname },
role = 'guest',
} = user
id: userId 表示读取 id 并存入变量 userId,不会同时声明 id。
默认值同样只在结果为 undefined 时生效:
js
const { count = 10 } = { count: null }
console.log(count) // null
工作落点与误区
- 接收配置对象、提取接口字段、交换变量、读取 React/Vue 组合式 API 返回值。
- 深层解构会让可选路径容易抛错,外部数据应先校验,或为中间层提供默认对象。
- 参数列表中解构十几个字段会降低可读性;复杂函数可以先接收
options,再在函数体分组提取。 - 解构是读取和赋值语法,不会自动克隆嵌套对象。
2.5 模板字符串与标签模板
模板字符串
js
const message = `用户 ${user.name} 有 ${orders.length} 个订单`
const html = `
<section>
<h2>${title}</h2>
</section>
`
插值表达式会正常求值并转成字符串。模板字符串不会自动防止 XSS,把用户输入插入 HTML 后交给 innerHTML 仍然危险。
标签模板
标签模板把静态片段和动态值分开交给函数:
js
function sql(strings, ...values) {
return {
text: strings.join('?'),
values,
}
}
const query = sql`
SELECT * FROM users
WHERE id = ${userId} AND status = ${status}
`
标签函数得到的 strings 是静态文本数组,values 是插值结果。实际数据库查询应使用成熟驱动的参数化 API;这个例子展示的是标签模板如何把结构与值分离,不代表手写 SQL 安全库。
工作中可见于 CSS-in-JS、GraphQL 查询、国际化和安全转义工具。
2.6 对象字面量增强
属性简写与方法简写
js
const id = 1
const name = 'Alice'
const user = {
id,
name,
greet() {
return `Hi, ${this.name}`
},
}
方法简写不只是少写 function,它还能在对象继承场景中使用 super。
计算属性名
js
const field = 'mobile'
const form = {
[field]: '13800138000',
[`${field}Error`]: '',
}
实际落点包括动态表单、按 ID 建索引、状态更新和协议字段生成:
js
function updateField(state, name, value) {
return {
...state,
[name]: value,
}
}
2.7 class、继承与 super
class 是原型机制的语法
js
class User {
constructor(name) {
this.name = name
}
greet() {
return `Hi, ${this.name}`
}
static createGuest() {
return new User('Guest')
}
}
实例方法实际位于 User.prototype:
js
const a = new User('A')
const b = new User('B')
a.greet === b.greet // true
Object.getPrototypeOf(a) === User.prototype // true
类声明存在 TDZ,类体默认严格模式,类必须使用 new 调用。
继承
js
class Admin extends User {
constructor(name, permissions) {
super(name)
this.permissions = permissions
}
greet() {
return `${super.greet()},权限:${this.permissions.join(',')}`
}
}
派生类构造函数在访问 this 前必须先调用 super(),因为基类构造过程负责创建和初始化实例。
工作落点
- 自定义 Error、领域实体、SDK 客户端、具有稳定继承关系的组件或框架基础类。
- 深继承树会把行为分散到多个层级,业务代码通常优先组合小对象和函数。
- ES2022 才正式加入类字段、私有字段和静态初始化块,不要把它们误记为原始 ES2015 class 的全部内容。
2.8 Symbol
Symbol() 创建唯一的原始值,常用于避免属性名冲突:
js
const cacheKey = Symbol('cache')
const anotherKey = Symbol('cache')
cacheKey === anotherKey // false
const target = {
[cacheKey]: new Map(),
}
全局 Symbol 注册表:
js
const a = Symbol.for('app.user')
const b = Symbol.for('app.user')
a === b // true
Symbol.keyFor(a) // 'app.user'
内置 well-known symbols 为语言协议提供扩展点,例如 Symbol.iterator、Symbol.toPrimitive、Symbol.toStringTag 和 Symbol.hasInstance。
Symbol 属性不会出现在 Object.keys 和普通 for...in 中,但可以通过 Object.getOwnPropertySymbols 或 Reflect.ownKeys 获取,所以它不是绝对私有机制。
2.9 Iterator、Iterable 与 for...of
两个协议
可迭代对象实现 [Symbol.iterator]();该方法返回迭代器。迭代器实现 next(),每次返回 { value, done }。
js
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from
const end = this.to
return {
next() {
if (current <= end) {
return { value: current++, done: false }
}
return { value: undefined, done: true }
},
}
},
}
console.log([...range]) // [1, 2, 3]
数组、字符串、Map、Set 都是内置可迭代对象。for...of 消费值,for...in 枚举属性键,两者不能混用:
js
for (const value of ['a', 'b']) {
console.log(value) // a、b
}
for (const key in ['a', 'b']) {
console.log(key) // 0、1,且类型是字符串
}
工作落点
自定义分页游标、树遍历、数据流和惰性序列时,可以统一接入展开语法、解构、for...of 等语言能力。需要注意:迭代器通常是一次性消费的,重新遍历可能需要重新创建。
2.10 Generator
生成器函数调用后不会立即执行函数体,而是返回生成器对象;每次 next() 让函数运行到下一个 yield:
js
function* createIds() {
let id = 1
while (true) {
yield id
id += 1
}
}
const ids = createIds()
ids.next() // { value: 1, done: false }
ids.next() // { value: 2, done: false }
yield* 可以委托另一个可迭代对象:
js
function* combined() {
yield 'start'
yield* [1, 2, 3]
yield 'end'
}
next(value) 传入的值会成为上一个 yield 表达式的结果,这使生成器能表达双向控制流,但也增加理解成本。
工作中可用于惰性序列、状态机、树遍历、测试数据生成和流程控制。普通异步业务现在通常优先使用 async/await;Generator 不应该只因为"高级"而使用。
2.11 Promise
状态模型
Promise 表示一个未来结果:
text
pending -> fulfilled
pending -> rejected
状态一旦确定不能改变。构造器中的 executor 立即同步执行,then 注册的反应通过微任务异步执行:
js
console.log('A')
const promise = new Promise((resolve) => {
console.log('B')
resolve(1)
})
promise.then(() => console.log('C'))
console.log('D')
// A B D C
链式调用
每次 then 都返回新的 Promise:
js
loadUser(id)
.then((user) => loadOrders(user.id))
.then((orders) => orders.filter((order) => order.paid))
.then(renderOrders)
.catch(showError)
回调返回普通值,新 Promise 以该值成功;返回 Promise 或 thenable,新 Promise 会采用其最终状态;抛出异常,新 Promise 拒绝。
忘记 return 会让后续链提前继续:
js
loadUser(id).then((user) => {
return loadOrders(user.id)
})
ES2015 静态方法
js
Promise.resolve(value)
Promise.reject(error)
Promise.all([requestA(), requestB()])
Promise.race([request(), timeout(5000)])
Promise.all 适合相互独立、必须全部成功的任务。它在一项失败时快速拒绝,但不会自动取消其余任务。Promise.race 也不会取消落败任务。
Promise 解决了异步结果组合与错误传播问题,但不会让 CPU 计算自动并行,也没有统一取消协议。
2.12 ES Module
具名导出与默认导出
js
// math.js
export const PI = 3.14159
export function add(a, b) {
return a + b
}
js
// app.js
import { add, PI } from './math.js'
js
// createApp.js
export default function createApp() {}
// main.js
import createApp from './createApp.js'
具名导出便于自动重构、静态分析和统一命名;默认导出适合模块确实只有一个核心概念时使用。
静态结构与 live binding
静态 import 和 export 位于模块顶层,工具可以在执行前建立依赖图。导入的是 live binding,不是简单复制值:
js
// counter.js
export let count = 0
export const increment = () => {
count += 1
}
js
import { count, increment } from './counter.js'
console.log(count) // 0
increment()
console.log(count) // 1
导入方不能直接给 count 赋值,但能观察导出模块对绑定的更新。
工作落点
- 按职责拆分模块,建立明确公共 API。
- 构建工具依据静态结构做 Tree Shaking,但是否真的移除还取决于副作用分析和包配置。
- 循环依赖虽然可能运行,但初始化顺序会变复杂,通常应通过职责拆分消除。
- 动态
import()是 ES2020 的正式特性,不属于 ES2015 原始模块语法。
2.13 Map、Set、WeakMap 与 WeakSet
Map
Map 的键可以是任意值,保留插入顺序,并提供清晰的 size:
js
const userById = new Map()
userById.set('1001', { name: 'Alice' })
userById.set(1001, { name: 'Bob' })
userById.size // 2,字符串和数字是不同键
userById.get('1001')
Map 适合动态键值集合、频繁增删、对象键缓存和索引。固定字段的领域对象仍更适合普通对象。
Set
js
const selectedIds = new Set(['1', '2'])
selectedIds.add('3')
selectedIds.has('2') // true
selectedIds.delete('1')
Set 适合唯一值集合、选择状态、权限集合和去重:
js
const uniqueIds = [...new Set(ids)]
Weak 集合
WeakMap 和 WeakSet 对对象保持弱引用,不阻止对象被垃圾回收,并且不可枚举:
js
const metadata = new WeakMap()
function getMetadata(element) {
if (!metadata.has(element)) {
metadata.set(element, { clickCount: 0 })
}
return metadata.get(element)
}
适合给 DOM、类实例或第三方对象附加生命周期一致的元数据。不可枚举是为了避免观察垃圾回收时机。
2.14 Proxy 与 Reflect
Proxy
Proxy 可以拦截对象的基础内部操作:
js
const state = new Proxy(
{ count: 0 },
{
get(target, key, receiver) {
console.log('读取', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
if (key === 'count' && value < 0) {
throw new RangeError('count 不能小于 0')
}
return Reflect.set(target, key, value, receiver)
},
},
)
常见 trap 包括 get、set、has、deleteProperty、ownKeys、apply 和 construct。
Reflect
Reflect 把许多底层对象操作变成函数,并返回更统一的结果:
js
Reflect.get(target, key, receiver)
Reflect.set(target, key, value, receiver)
Reflect.has(target, key)
Reflect.ownKeys(target)
Reflect.construct(User, ['Alice'])
Proxy trap 中使用 Reflect 可以保留默认语义,并正确传递 receiver。
工作落点与边界
- Vue 3 一类响应式系统、校验层、API 客户端、虚拟对象和底层库。
- Proxy 默认只代理当前对象。深层响应式通常要在读取嵌套对象时继续包装,并处理缓存和身份一致性。
- 内建对象有内部槽,例如某些 Map 方法不能随意借给代理对象使用,需要正确绑定接收者。
- Proxy 无法完整 polyfill,必须确认目标运行环境。
2.15 ES2015 新增的常用内置 API
Array
js
Array.from(arrayLike, mapFn)
Array.of(1, 2, 3)
users.find((user) => user.id === id)
users.findIndex((user) => user.id === id)
new Array(3).fill(0)
items.copyWithin(target, start, end)
Array.from 可把类数组或可迭代对象变成数组,并在创建时映射:
js
const indexes = Array.from({ length: 5 }, (_, index) => index)
// [0, 1, 2, 3, 4]
fill 填充对象时会重复同一个引用:
js
const wrong = new Array(3).fill({ selected: false })
wrong[0].selected = true
console.log(wrong[1].selected) // true
const correct = Array.from(
{ length: 3 },
() => ({ selected: false }),
)
Object
js
Object.assign(target, sourceA, sourceB)
Object.is(NaN, NaN) // true
Object.setPrototypeOf(target, prototype)
Object.assign 复制自身可枚举属性,并调用目标对象的 setter,是浅层操作。不可变更新时优先写入新对象:
js
const next = Object.assign({}, defaults, options)
String 与 Unicode
js
text.includes(keyword)
text.startsWith('https://')
text.endsWith('.json')
'ha'.repeat(3) // hahaha
'😀'.codePointAt(0)
String.fromCodePoint(0x1f600)
'\u{1F600}' // 😀,需要 u 语义支持
ES2015 的 Unicode 改进缓解了 UTF-16 代理对处理问题,但 string.length 仍按 UTF-16 code unit 计数,不等同于用户看到的字符数。
Number 与 Math
js
Number.isNaN(value)
Number.isFinite(value)
Number.isInteger(value)
Number.isSafeInteger(value)
Number.parseInt('10', 10)
Math.trunc(3.9) // 3
Math.sign(-5) // -1
Math.cbrt(8) // 2
Math.hypot(3, 4) // 5
Number.isNaN 和 Number.isFinite 不会先隐式转换,通常比全局同名函数更适合做类型明确的业务校验。
二进制和八进制字面量:
js
0b1010 // 10
0o755 // 493
RegExp 的 u 与 y 标志
u 开启更完整的 Unicode 语义;y 是 sticky 模式,只从 lastIndex 指定位置开始匹配,适合词法分析器等连续解析场景。
js
const token = /\d+/y
token.lastIndex = 2
token.exec('ab12cd') // 匹配 12
这些能力在普通页面中不如数组和 Promise 高频,但在解析器、编辑器、国际化文本处理和底层库中很重要。
2.16 ES2015 的工程总结
ES2015 真正改变的不只是语法长度,而是程序组织方式:
text
let/const -> 更明确的作用域和绑定
箭头函数 -> 更轻量回调与词法 this
解构/默认参数 -> 更清晰的数据边界
class -> 统一构造与继承表达
Iterator -> 统一数据消费协议
Promise -> 可组合的异步结果
ES Module -> 静态依赖与模块边界
Map/Set -> 更准确的数据结构
Proxy/Reflect -> 元编程和框架基础设施
现代项目不应为了"使用 ES6"而把每项特性都塞进代码。正确做法是让语法表达真实语义:稳定绑定用 const,动态键值集合用 Map,唯一集合用 Set,需要静态依赖图时用 ESM,需要异步组合时用 Promise。
三、ES2016:小版本中的两个高频能力
3.1 指数运算符 **
js
2 ** 3 // 8
2 ** 3 ** 2 // 512
指数运算符是右结合,第二个表达式等价于:
js
2 ** (3 ** 2)
它对应 Math.pow 的常见用途:
js
Math.pow(base, exponent)
base ** exponent
复合赋值:
js
let value = 3
value **= 2
console.log(value) // 9
一元负号与指数运算组合时需要括号:
js
-(2 ** 2) // -4
(-2) ** 2 // 4
工作中可用于复利、距离、面积、统计和算法计算。普通业务不应为了少写几个字符牺牲可读性,复杂数学表达式应拆成有业务名称的中间变量。
3.2 Array.prototype.includes
includes 判断数组是否包含某个值:
js
const roles = ['admin', 'editor']
roles.includes('admin') // true
roles.includes('guest') // false
第二个参数指定开始搜索的位置,也支持负数:
js
['a', 'b', 'c'].includes('a', 1) // false
['a', 'b', 'c'].includes('c', -1) // true
它与 indexOf 的重要差异是可以找到 NaN:
js
[NaN].indexOf(NaN) // -1
[NaN].includes(NaN) // true
原因是 includes 使用 SameValueZero 比较,NaN 与自身视为相等,0 与 -0 也视为相等。
工作落点
js
const ALLOWED_STATUSES = ['draft', 'published', 'archived']
if (!ALLOWED_STATUSES.includes(status)) {
throw new Error('不支持的状态')
}
- 小型固定列表可直接使用
includes,语义比indexOf(...) !== -1清晰。 - 大型集合需要频繁查询时,预先建立
Set,平均查询成本更合适。 - 对象数组按 ID 查找不能直接传入新对象,应使用
some或 Map:users.some((user) => user.id === id)。
四、ES2017:异步代码进入直线表达时代
4.1 async/await
基本概念
async/await 建立在 Promise 之上,让异步流程可以按接近同步代码的结构书写。async 函数总是返回 Promise:
js
async function getValue() {
return 42
}
getValue().then(console.log) // 42
如果函数抛错,返回的 Promise 会拒绝:
js
async function fail() {
throw new Error('failed')
}
fail().catch((error) => console.log(error.message))
await 的暂停与恢复
await expression 会先把表达式按 Promise 语义处理。当前 async 函数暂停,把控制权交还调用方;结果确定后,函数后半段通过微任务恢复:
js
async function run() {
console.log('B')
const value = await Promise.resolve(1)
console.log('D', value)
}
console.log('A')
run()
console.log('C')
// A B C D 1
await 不会阻塞整个 JavaScript 线程,只暂停当前 async 函数。其他同步代码、事件和任务仍可以继续。
错误处理
js
async function loadUserPage(id) {
startLoading()
try {
const user = await fetchUser(id)
const orders = await fetchOrders(user.id)
return { user, orders }
} catch (error) {
throw new Error('加载用户页面失败', { cause: error })
} finally {
stopLoading()
}
}
await 的 Promise 拒绝会表现为当前函数中抛出异常,所以能用 try/catch/finally。不要在每一层都捕获并只打印日志,否则上层会误以为操作成功。
串行、并行与依赖
有依赖时串行:
js
const user = await fetchUser(id)
const orders = await fetchOrders(user.id)
无依赖时并行启动:
js
const [user, config, permissions] = await Promise.all([
fetchUser(id),
fetchConfig(),
fetchPermissions(id),
])
下面的写法会无意串行:
js
const user = await fetchUser(id)
const config = await fetchConfig()
如果两个请求分别需要 800ms 和 600ms,串行约需 1400ms,并行约由较慢的 800ms 决定。
循环中的 await
串行处理:
js
for (const file of files) {
await upload(file)
}
全部并行:
js
await Promise.all(files.map((file) => upload(file)))
不要使用 forEach(async () => {}) 等待任务:
js
// 外层不会等待回调返回的 Promise
files.forEach(async (file) => {
await upload(file)
})
批量任务数量较大时,应限制并发,而不是一次发出全部请求:
js
async function runPool(tasks, limit = 4) {
const results = new Array(tasks.length)
let nextIndex = 0
async function worker() {
while (nextIndex < tasks.length) {
const index = nextIndex
nextIndex += 1
results[index] = await tasks[index]()
}
}
const workers = Array.from(
{ length: Math.min(limit, tasks.length) },
() => worker(),
)
await Promise.all(workers)
return results
}
工作落点
- 页面初始化时组合多个接口。
- 表单提交中的校验、上传、保存和跳转。
- Node.js 服务端中的数据库与远程服务调用。
- 定时任务和脚本中的顺序流程。
async/await 只是 Promise 的控制流语法,不提供超时、取消、重试和并发限制。这些能力仍要通过 AbortSignal、调度器和业务策略实现。
4.2 Object.values 与 Object.entries
js
const scoreByUser = {
alice: 90,
bob: 80,
}
Object.values(scoreByUser) // [90, 80]
Object.entries(scoreByUser) // [['alice', 90], ['bob', 80]]
配合 for...of:
js
for (const [name, score] of Object.entries(scoreByUser)) {
console.log(`${name}: ${score}`)
}
它们只处理对象自身的可枚举字符串键,不包含继承属性和 Symbol 键。数字形式的属性键在结果中仍是字符串。
工作落点
js
const query = new URLSearchParams(
Object.entries(filters).filter(([, value]) => value != null),
)
适用于配置遍历、字典转换、查询参数、表单错误列表和统计聚合。需要任意类型键时应使用 Map,而不是在对象与 entries 之间反复转换。
4.3 字符串填充
js
'7'.padStart(2, '0') // '07'
'JS'.padEnd(5, '.') // 'JS...'
目标长度按 UTF-16 code unit 计算,填充字符串会重复并在需要时截断:
js
'A'.padStart(5, '01') // '0101A'
工作中可用于时间、序号、表格文本和日志格式:
js
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
货币和本地化数字展示优先使用 Intl.NumberFormat,不要手写补齐与分隔逻辑。
4.4 Object.getOwnPropertyDescriptors
Object.assign 和对象展开主要复制值,不完整保留 getter、setter、可枚举性等属性描述符:
js
const source = {
get total() {
return 100
},
}
const descriptors = Object.getOwnPropertyDescriptors(source)
const clone = Object.defineProperties({}, descriptors)
适合框架、装饰器、mixin 和需要精确复制属性语义的底层工具。普通业务对象通常不需要为此增加复杂度。
4.5 函数参数与调用语法的尾逗号
js
function createUser(
id,
name,
) {}
createUser(
1,
'Alice',
)
尾逗号让新增参数时 Git diff 更干净,也便于格式化工具统一多行结构。剩余参数后不能再加逗号。
4.6 SharedArrayBuffer 与 Atomics
SharedArrayBuffer 允许多个执行代理共享同一块内存,Atomics 提供原子读写和同步能力:
js
const shared = new SharedArrayBuffer(4)
const counter = new Int32Array(shared)
Atomics.add(counter, 0, 1)
Atomics.load(counter, 0) // 1
它解决的是真正并发读写问题,而不是普通页面状态管理。典型场景包括并行计算、WebAssembly、底层运行时和高性能处理。
共享内存会带来竞态、可见性和死锁风险。浏览器中还通常要求跨源隔离响应头。普通表单、列表和业务状态不应使用它。
五、ES2018:异步流、对象展开与正则全面增强
5.1 异步迭代器与 for await...of
同步迭代器的 next() 返回普通结果;异步迭代器的 next() 返回 Promise:
js
const asyncIterable = {
[Symbol.asyncIterator]() {
let page = 1
return {
async next() {
if (page > 3) {
return { done: true, value: undefined }
}
const value = await fetchPage(page)
page += 1
return { done: false, value }
},
}
},
}
消费方式:
js
for await (const pageData of asyncIterable) {
renderPage(pageData)
}
更常见的是异步生成器:
js
async function* paginate(loadPage) {
let page = 1
while (true) {
const result = await loadPage(page)
yield* result.items
if (!result.hasNext) return
page += 1
}
}
for await (const item of paginate(fetchOrders)) {
await processOrder(item)
}
工作落点
- 游标分页、流式响应、日志流和消息队列。
- Node.js 可读流和 Web Streams 的逐块处理。
- 避免一次把所有数据加载进内存。
for await...of 默认逐项等待,适合有顺序或背压需求的流。互不依赖的有限任务如果追求吞吐量,应采用受控并发。
5.2 对象 Rest 与 Spread
对象展开
js
const defaults = {
timeout: 5000,
retry: 0,
}
const options = {
...defaults,
retry: 2,
}
后面的同名属性覆盖前面的属性。对象展开复制自身可枚举属性,包含可枚举 Symbol 键,但不复制原型和不可枚举属性。
对象剩余属性
js
const { password, token, ...publicUser } = user
适合从对象中排除敏感字段,但要注意这是浅层新对象:嵌套对象仍共享引用。
undefined 覆盖默认值
js
const merged = {
timeout: 5000,
...{ timeout: undefined },
}
console.log(merged.timeout) // undefined
对象展开按"属性是否存在"覆盖,不会自动忽略 undefined。配置合并如果希望只接受有效值,应先过滤或显式使用 ??。
工作落点
- React/Vue 状态的浅层不可变更新。
- 请求参数和组件 props 合并。
- DTO 去除字段、按条件增加字段。
js
const payload = {
name: form.name,
...(form.mobile ? { mobile: form.mobile } : {}),
}
深层状态仍需复制每个变化层级;不要把对象展开当成深拷贝。
5.3 Promise.prototype.finally
finally 无论 Promise 成功还是失败都执行,适合清理与结束状态:
js
startLoading()
saveForm()
.then(showSuccess)
.catch(showError)
.finally(stopLoading)
finally 回调不接收成功值或失败原因,正常返回时保留原结果:
js
Promise.resolve('data')
.finally(() => console.log('cleanup'))
.then(console.log) // data
如果 finally 抛错或返回拒绝的 Promise,新错误会覆盖原结果。因此清理函数本身也应可靠,不要在其中执行无关高风险业务。
5.4 正则表达式增强
dotAll s 标志
默认的 . 不匹配换行符,s 标志让它匹配换行:
js
/start.*end/.test('start\nend') // false
/start.*end/s.test('start\nend') // true
解析 HTML、JSON 和代码优先使用结构化解析器,不能因为 dotAll 更方便就依赖脆弱的巨大正则。
命名捕获组
js
const result = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/.exec(
'2026-08-13',
)
console.log(result.groups.year) // 2026
console.log(result.groups.month) // 08
替换中可以使用名称:
js
'2026-08-13'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>',
)
名称比 $1、$2 更能表达业务含义,也不容易在增加捕获组后错位。
后行断言
js
/(?<=¥)\d+/.exec('价格 ¥100')[0] // 100
/(?<!¥)\d+/.exec('数量 20')[0] // 20
(?<=...) 是正向后行断言,(?<!...) 是负向后行断言。断言检查上下文但不把上下文本身包含进匹配结果。
Unicode 属性转义
js
/\p{Script=Han}+/u.test('中文') // true
/\p{Letter}+/u.test('Hello世界') // true
/\p{Emoji}/u.test('😀') // true
相比手写字符区间,Unicode 属性更能表达"字母""汉字""数字"等语义。但 Unicode 数据会随规范演进,输入校验仍要以业务允许范围为准。
5.5 模板字符串转义规则修订
ES2018 允许标签模板接收某些包含非法转义序列的原始文本,对应 cooked 值为 undefined,strings.raw 仍保留原始内容。这主要服务 DSL、LaTeX 等标签模板库。
js
function inspect(strings) {
console.log(strings[0])
console.log(strings.raw[0])
}
普通模板字符串并没有因此允许任意非法转义;该能力的实际落点主要在工具和库作者。
六、ES2019:数据转换与语言细节打磨
6.1 Array.prototype.flat
js
const nested = [1, [2, [3, [4]]]]
nested.flat() // [1, 2, [3, [4]]]
nested.flat(2) // [1, 2, 3, [4]]
nested.flat(Infinity) // [1, 2, 3, 4]
默认深度是 1。flat 返回新数组,并会移除它处理层级中的稀疏位置。
工作中可用于树形数据的有限层展开、接口批次结果合并和矩阵处理。深度未知的大型嵌套数据使用 Infinity 前应考虑内存、递归深度和数据设计是否合理。
6.2 Array.prototype.flatMap
flatMap 等价于先 map 再进行一层 flat 的常见语义,但实现可避免中间数组:
js
const departments = [
{ name: '研发', users: ['Alice', 'Bob'] },
{ name: '设计', users: ['Carol'] },
]
const users = departments.flatMap((department) =>
department.users.map((name) => ({
department: department.name,
name,
})),
)
还可通过返回空数组、单项数组或多项数组同时表达过滤和映射:
js
const positiveDoubles = numbers.flatMap((number) =>
number > 0 ? [number * 2] : [],
)
如果只需一对一转换,用 map 更清楚;flatMap 的价值在一对零、一对多和嵌套结果合并。
6.3 Object.fromEntries
它是 Object.entries 的反向转换:
js
const object = Object.fromEntries([
['name', 'Alice'],
['role', 'admin'],
])
转换对象值:
js
const normalized = Object.fromEntries(
Object.entries(form).map(([key, value]) => [
key,
typeof value === 'string' ? value.trim() : value,
]),
)
过滤查询参数:
js
const cleanFilters = Object.fromEntries(
Object.entries(filters).filter(([, value]) => value != null),
)
键会按对象属性规则转成字符串或 Symbol。需要保留对象键时使用 new Map(entries)。
6.4 可选的 catch 绑定
不需要错误对象时可以省略参数:
js
try {
return JSON.parse(text)
} catch {
return fallback
}
仅在错误确实无须区分和上报时使用。解析关键配置、支付和数据写入失败时,静默回退可能掩盖严重问题。
6.5 trimStart 与 trimEnd
js
' hello '.trimStart() // 'hello '
' hello '.trimEnd() // ' hello'
trimLeft 和 trimRight 通常作为兼容别名存在,新代码优先使用不依赖书写方向的 trimStart、trimEnd。
工作中常见于文本输入和文件内容处理。用户名、密码等字段是否允许或保留空格要按业务规则决定,不应统一对全部字段 trim。
6.6 Symbol.prototype.description
js
const key = Symbol('cache')
key.description // 'cache'
Symbol().description // undefined
description 主要用于调试和日志,不参与 Symbol 唯一性,也不应被当成稳定业务 ID。
6.7 JSON、排序与函数源码的一致性改进
ES2019 还包含几项不一定直接写出新 API、但影响可靠性的规范改进:
- JSON 成为 JavaScript 源文本的语法子集,相关行分隔字符处理更一致。
JSON.stringify对孤立 UTF-16 代理项采用良构转义,减少编码传输问题。Array.prototype.sort被要求稳定排序:比较结果为 0 的元素保留原相对顺序。Function.prototype.toString更准确返回函数源文本表示。
稳定排序的工作意义:
js
const users = [
{ name: 'A', level: 2 },
{ name: 'B', level: 1 },
{ name: 'C', level: 2 },
]
users.sort((a, b) => a.level - b.level)
// A 和 C 的相对顺序保持不变
不过 sort 会修改原数组;状态数据应在 ES2023 环境使用 toSorted,或先复制再排序。
七、ES2020:现代业务代码中最常见的一批增强
7.1 可选链 ?.
属性、索引和调用
js
user?.profile?.address?.city
users?.[selectedIndex]
onSuccess?.(result)
当可选链左侧是 null 或 undefined 时,当前连续链短路并返回 undefined,不会继续访问后续属性。
js
const user = null
let index = 0
user?.items[index++]
console.log(index) // 0,索引表达式没有执行
连续链边界
括号可能切断可选链:
js
const value = user?.profile?.name
// (user?.profile).name
// 如果 profile 不存在,括号后的 .name 仍会抛错
方法调用还要区分"对象可能为空"和"方法可能为空":
js
service?.save() // service 可为空;存在时要求 save 可调用
service.save?.() // service 必须存在;save 可为空
工作落点与误区
- 读取后端确实允许缺失的嵌套字段。
- 调用可选回调和插件钩子。
- 访问可能缺失的 Map 查询结果后续属性。
可选链不应该掩盖系统不变量。核心配置和必填接口字段缺失时,尽早校验并抛出清晰错误,比一路得到 undefined 更容易排查。
7.2 空值合并 ??
?? 只在左侧是 null 或 undefined 时使用右侧默认值:
js
0 ?? 10 // 0
'' ?? 'N/A' // ''
false ?? true // false
null ?? 10 // 10
对比 ||:
js
0 || 10 // 10
0 ?? 10 // 0
页码、价格、开关和空字符串都可能是合法值,因此配置默认值通常优先 ??。
?? 不能不加括号地直接和 &&、|| 混用:
js
const result = enabled && (value ?? fallback)
这项语法限制是为了避免读者误判优先级。
7.3 BigInt
BigInt 表示任意精度整数:
js
const id = 900719925474099312345n
const next = id + 1n
BigInt('900719925474099312345')
BigInt 和 Number 不能直接混合算术:
js
// 1n + 1 // TypeError
1n + BigInt(1)
除法会截断小数部分:
js
5n / 2n // 2n
BigInt 不能直接交给 Math 方法,JSON.stringify 默认也不能序列化:
js
const json = JSON.stringify(
{ id },
(key, value) => typeof value === 'bigint' ? value.toString() : value,
)
工作落点
- 超过
Number.MAX_SAFE_INTEGER的整数计算。 - 密码学、区块链、精确计数和底层协议。
- 超长业务 ID 如果只用于标识和传输,通常直接使用字符串更稳妥,无需 BigInt 运算。
接口必须从源头返回字符串或 BigInt 可安全构造的文本。一个超长 ID 如果先作为 JSON Number 被解析,精度已经丢失,再转 BigInt 也无法恢复。
7.4 动态 import()
动态导入返回 Promise,可在运行时按需加载模块:
js
async function openEditor() {
const { createEditor } = await import('./heavy-editor.js')
return createEditor()
}
工作落点:
- 路由级代码分割。
- 富文本编辑器、图表、地图等低频重型功能。
- 根据地区、主题或运行环境加载实现。
js
const localeModule = await import(`./locales/${locale}.js`)
动态路径可能影响构建工具的静态分析,路径范围应保持可枚举和受控。首次点击才加载会产生等待,关键功能可结合预加载策略。
静态 import 适合确定依赖,动态 import() 适合条件或延迟依赖,不应为了"懒加载"把所有模块都改成动态导入。
7.5 Promise.allSettled
它等待所有输入结束,不会因为某一项失败提前拒绝:
js
const results = await Promise.allSettled([
upload(fileA),
upload(fileB),
upload(fileC),
])
for (const result of results) {
if (result.status === 'fulfilled') {
console.log('成功', result.value)
} else {
console.error('失败', result.reason)
}
}
结果是区分联合结构:
text
{ status: 'fulfilled', value }
{ status: 'rejected', reason }
适合批量上传、批量通知、多个可选模块初始化和允许部分失败的后台操作。如果任何失败都应终止整体流程,使用 Promise.all 更符合语义。
7.6 globalThis
不同环境过去需要使用不同全局对象:浏览器的 window、Worker 的 self、Node.js 的 global。globalThis 提供统一访问方式:
js
globalThis.setTimeout
globalThis.console
通用库、polyfill 和跨运行时工具会使用它。业务模块不应把状态随意挂到全局对象,否则会造成命名冲突、测试污染和隐藏依赖。
7.7 String.prototype.matchAll
matchAll 返回包含全部匹配详情的迭代器,正则需要 g 标志:
js
const text = 'id=12&count=30'
const pattern = /(?<key>\w+)=(?<value>\d+)/g
for (const match of text.matchAll(pattern)) {
console.log(match.groups.key, match.groups.value)
}
与只返回匹配文本的 match 全局模式相比,matchAll 保留捕获组、索引和输入信息,也避免手写 exec 循环时错误处理 lastIndex。
工作中适合日志抽取、轻量模板标记和文本分析。结构化格式仍应使用 URL、JSON、HTML 或语言解析器。
7.8 import.meta
import.meta 提供当前模块相关的宿主元数据。浏览器和现代构建工具常见的是 import.meta.url:
js
const worker = new Worker(
new URL('./worker.js', import.meta.url),
{ type: 'module' },
)
它让相对资源 URL 以当前模块为基准解析,适用于 Worker、图片、WASM 和模块旁资源。import.meta.env 等字段通常是构建工具约定,不属于 ECMAScript 统一标准,不能把特定工具行为当成语言能力。
7.9 for...in 枚举顺序规范化
ES2020 进一步规范了 for...in 的属性枚举顺序约束。但业务逻辑仍不应过度依赖复杂对象原型链的枚举顺序。
自身数据使用 Object.keys、Object.entries;需要稳定业务顺序时使用数组或 Map;避免用 for...in 遍历数组。
7.10 模块命名空间转导出
可以把另一个模块的全部导出作为一个命名空间重新导出:
js
// index.js
export * as userApi from './user-api.js'
export * as orderApi from './order-api.js'
使用方:
js
import { userApi, orderApi } from './index.js'
await userApi.getUser(id)
await orderApi.getOrders(id)
过去需要先导入再导出:
js
import * as userApi from './user-api.js'
export { userApi }
适合 SDK 入口和按领域组织的公共模块。不要把所有文件都堆进一个巨大命名空间,这会削弱具名导出的 Tree Shaking、自动导入和依赖可见性。
八、ES2021:默认值、批量替换与 Promise 竞速
8.1 逻辑赋值运算符
ES2021 加入:
js
value ||= fallback
value &&= nextValue
value ??= fallback
它们不是普通赋值与逻辑表达式的简单字符缩写,而是只在条件满足时执行右侧并赋值。
||=:左侧为假值时赋值
js
let title = ''
title ||= '未命名'
console.log(title) // 未命名
0、false 和空字符串都会触发 ||=,所以它适合"所有假值都代表缺失"的情况。
??=:左侧为空值时赋值
js
const config = {
retry: 0,
}
config.retry ??= 3
config.timeout ??= 5000
console.log(config) // { retry: 0, timeout: 5000 }
??= 只把 null 和 undefined 当作缺失,更适合表单数字、开关和配置默认值。
&&=:左侧为真值时赋值
js
let callback = () => 'done'
callback &&= wrapWithLogging(callback)
它在左侧为真值时才计算并写入右侧结果。
左侧只求值一次
这一点对 getter、setter 和计算属性很重要:
js
const state = {
_value: null,
get value() {
console.log('get')
return this._value
},
set value(next) {
console.log('set', next)
this._value = next
},
}
state.value ??= 10
// get
// set 10
如果原值已经满足短路条件,右侧和 setter 都不会执行。工作中不要在逻辑赋值右侧放依赖必定执行的副作用。
8.2 String.prototype.replaceAll
js
const path = 'users/:id/orders/:id'
path.replaceAll(':id', '1001')
// users/1001/orders/1001
过去常见写法是 split(search).join(replacement) 或全局正则。replaceAll 直接表达替换全部字面文本。
如果传入 RegExp,它必须带 g 标志:
js
'a1b2'.replaceAll(/\d/g, '#') // a#b#
// 'a1b2'.replaceAll(/\d/, '#') // TypeError
字符串搜索值不会按正则解释,所以替换用户输入时通常更稳妥:
js
function highlightPlainText(text, keyword) {
return text.replaceAll(keyword, `[${keyword}]`)
}
但这不等于 HTML 安全。结果如果进入 innerHTML,仍需正确转义或清洗。
替换字符串中的 $&、$1 等仍具有特殊含义;需要原样插入复杂替换值时,可使用函数:
js
text.replaceAll(keyword, () => replacement)
8.3 Promise.any 与 AggregateError
Promise.any 返回第一个成功的结果,只有全部失败才拒绝:
js
const data = await Promise.any([
fetchFromPrimary(),
fetchFromBackupA(),
fetchFromBackupB(),
])
与 Promise.race 对比:
text
Promise.race:第一个确定状态的结果,不管成功还是失败
Promise.any:忽略前面的失败,等待第一个成功
全部失败时得到 AggregateError:
js
try {
await Promise.any([
Promise.reject(new Error('A failed')),
Promise.reject(new Error('B failed')),
])
} catch (error) {
console.log(error instanceof AggregateError) // true
console.log(error.errors) // 两个失败原因
}
工作落点
- 多个 CDN 或镜像源取第一个成功响应。
- 主服务与降级服务并行探测。
- 多种可替代的数据获取策略。
它不会取消其他仍在运行的任务。拿到第一个成功结果后若要节省资源,需要业务自己通过 AbortController 取消其余请求。并行访问多个服务还会增加流量,不能只看响应速度。
8.4 数字分隔符
js
const budget = 1_000_000
const mask = 0b1111_0000
const color = 0xff_00_cc
const huge = 9_007_199_254_740_993n
下划线只提高源码可读性,不改变数值。不能放在数字开头、结尾、小数点旁或连续使用。
它只属于数字字面量语法:
js
Number('1_000') // NaN
用户输入和接口文本不会自动支持分隔符。金额、权限位、时间常量和大整数源码是最实际的使用位置。
8.5 WeakRef 与 FinalizationRegistry
WeakRef
js
let target = { id: 1 }
const weakRef = new WeakRef(target)
weakRef.deref() // 可能得到 target
target = null
// 未来某次 GC 后,deref() 可能得到 undefined
WeakRef 不阻止对象被回收,但垃圾回收时机不可预测。同一次同步作业中对象通常不会在观察之间突然消失,但业务仍必须把 deref() 结果当作可空值。
FinalizationRegistry
js
const registry = new FinalizationRegistry((heldValue) => {
console.log('对象已被回收', heldValue)
})
let object = {}
registry.register(object, 'debug-id-1')
object = null
回调是否执行、何时执行都不确定,程序退出前甚至可能完全不执行。
工作落点与限制
- 底层缓存、对象互操作层和诊断工具。
- 不能用于保存数据、关闭关键连接、网络上报、扣款或任何必须发生的流程。
- 普通缓存优先使用容量限制、过期时间和明确清理;只有对象生命周期确实需要弱关联时才考虑这些 API。
九、ES2022:类系统完善与模块级异步
9.1 公有类字段
js
class Counter {
count = 0
label = '默认计数器'
increment() {
this.count += 1
}
}
公有字段成为实例自身属性,而类方法位于原型上:
js
const counter = new Counter()
Object.hasOwn(counter, 'count') // true
Object.hasOwn(counter, 'increment') // false
字段初始化按源码顺序发生。基类实例字段在基类构造函数体执行前初始化;派生类实例字段在 super() 返回后初始化。
箭头函数类字段会为每个实例创建新函数并捕获实例 this:
js
class Form {
handleSubmit = () => {
this.submit()
}
}
它适合把方法直接作为回调,但实例很多时会产生更多函数对象,也无法像原型方法那样自然共享和覆写。不要把所有方法机械改成箭头字段。
9.2 私有字段、方法与访问器
js
class Wallet {
#balance = 0
deposit(amount) {
if (amount <= 0) throw new RangeError('金额必须大于 0')
this.#balance += amount
}
get balance() {
return this.#balance
}
#audit(action) {
console.log(action)
}
}
#balance 由语言强制私有,不是普通字符串属性:
js
const wallet = new Wallet()
// wallet.#balance // SyntaxError:类外无法引用该私有名
wallet['#balance'] = 100 // 只是新建一个普通字符串属性
私有字段必须在类体中声明,不能动态添加,也不会参与原型链普通属性查找。
私有品牌检查
js
class Wallet {
#balance = 0
static isWallet(value) {
return #balance in value
}
}
#balance in value 检查对象是否拥有这个类定义的私有品牌,比尝试访问再捕获异常更清晰。
工作落点
- SDK 中不能被调用方随意修改的内部状态。
- 需要维护不变量的领域对象。
- 库作者隐藏缓存和实现细节。
它会限制测试替换、序列化和继承扩展。普通页面 DTO 不需要为了"封装"全部改成私有类。
9.3 静态字段与静态初始化块
js
class Config {
static environment = 'production'
static #values
static {
const raw = globalThis.APP_CONFIG ?? {}
this.#values = Object.freeze({ ...raw })
}
static get(key) {
return this.#values[key]
}
}
静态块在类定义求值时执行,可以访问私有静态字段,并使用语句、异常处理和局部变量完成复杂初始化。
适用于注册表、兼容层和需要一次性计算的类级配置。静态块包含网络、文件等重副作用会让模块加载不可预测,异步操作也不能直接 await;这类初始化应保持轻量。
9.4 顶层 await
ES Module 顶层可以直接使用 await:
js
// config.js
const response = await fetch('/config.json')
export const config = await response.json()
依赖该模块的模块会等待它完成求值:
js
// app.js
import { config } from './config.js'
startApp(config)
工作落点
- 应用启动前必须完成的配置、WASM 和本地化资源初始化。
- Node.js 脚本中的启动数据读取。
- 条件动态导入。
风险
顶层 await 会把异步等待传播到依赖图,可能延迟一大片模块执行;循环依赖中还可能形成难以分析的等待关系。非关键资源优先导出初始化 Promise 或异步函数,让调用方决定等待时机。
顶层 await 只允许在 Module 中,不适用于普通 Script。
9.5 .at() 相对索引
Array、String 和 TypedArray 都支持 at:
js
const items = ['a', 'b', 'c']
items.at(0) // a
items.at(-1) // c
items.at(-2) // b
'JavaScript'.at(-1) // t
相比 items[items.length - 1],负数索引更直接。方括号 items[-1] 读取的是名为 '-1' 的普通属性,不是倒数第一项。
空数组 at(-1) 返回 undefined,调用后续方法前仍需处理空值。
9.6 Object.hasOwn
js
Object.hasOwn(object, key)
它判断自身属性,不沿原型链,并适用于没有原型或覆盖了 hasOwnProperty 的对象:
js
const dictionary = Object.create(null)
dictionary.name = 'Alice'
Object.hasOwn(dictionary, 'name') // true
对比:
text
key in object 自身或原型链存在即可
Object.hasOwn(object,key) 只检查自身属性
处理配置合并、不可信输入、字典和序列化数据时,明确是否接受继承属性很重要。
9.7 Error Cause
包装错误时可以保留原始原因:
js
async function loadConfig() {
try {
return await requestConfig()
} catch (error) {
throw new Error('应用配置加载失败', {
cause: error,
})
}
}
上层得到业务上下文,日志系统仍能沿 error.cause 查看底层网络或解析错误。
自定义错误也可以转交 options:
js
class DataAccessError extends Error {
constructor(message, options) {
super(message, options)
this.name = 'DataAccessError'
}
}
不要把令牌、完整请求体或个人敏感数据放进错误链并上报。
9.8 RegExp 匹配索引 /d
d 标志让匹配结果包含每个捕获组的起止位置:
js
const pattern = /(?<key>\w+)=(?<value>\d+)/d
const match = pattern.exec('count=42')
match.indices[0] // [0, 8]
match.indices.groups.key // [0, 5]
match.indices.groups.value // [6, 8]
适合编辑器高亮、语法分析、搜索标记和错误定位。只需要判断是否匹配时不要加 d,避免生成不需要的索引数据。
十、ES2023:不可变数组操作成为标准能力
10.1 findLast 与 findLastIndex
js
const logs = [
{ type: 'info', id: 1 },
{ type: 'error', id: 2 },
{ type: 'error', id: 3 },
]
logs.findLast((log) => log.type === 'error')
// { type: 'error', id: 3 }
logs.findLastIndex((log) => log.type === 'error')
// 2
它们从数组末尾向前搜索,找到后立即停止。适合读取最后一次状态、最近事件和撤销记录,比 filter(...).at(-1) 更节省中间数组,也更准确表达意图。
10.2 Change Array by Copy
传统 reverse、sort 和 splice 会修改原数组,ES2023 增加返回新数组的对应方法。
toReversed
js
const original = [1, 2, 3]
const reversed = original.toReversed()
console.log(original) // [1, 2, 3]
console.log(reversed) // [3, 2, 1]
toSorted
js
const sortedUsers = users.toSorted(
(a, b) => b.score - a.score,
)
与 sort 一样,默认按字符串比较;数字排序仍要提供比较函数。
toSpliced
js
const next = items.toSpliced(index, 1, replacement)
它返回删除或插入后的新数组,而 splice 返回被删除元素并修改原数组。
js
const inserted = items.toSpliced(2, 0, newItem)
const removed = items.toSpliced(2, 1)
with
js
const next = items.with(index, updatedItem)
支持负数索引:
js
[1, 2, 3].with(-1, 9) // [1, 2, 9]
索引越界会抛 RangeError,这能更早暴露状态计算错误。
工作落点
这些方法非常适合 React、Vue 状态、Redux reducer、缓存快照和任何依赖引用变化检测的场景:
js
function updateUser(users, index, patch) {
return users.with(index, {
...users[index],
...patch,
})
}
它们只复制数组容器,元素对象仍共享引用。不可变更新嵌套对象时仍要复制变化对象。
10.3 Hashbang Grammar
JavaScript 文件第一行可以使用 Unix shebang:
js
#!/usr/bin/env node
console.log('CLI started')
这让 Node.js CLI 脚本可以直接作为可执行文件。Hashbang 只能位于源码开头;它主要服务命令行运行时,对浏览器业务没有实际用途。
10.4 Weak 集合支持更多 Symbol 键
ES2023 允许不可注册的 Symbol 作为 WeakMap 键和 WeakSet 成员:
js
const key = Symbol('temporary')
const cache = new WeakMap()
cache.set(key, { value: 1 })
通过 Symbol.for 创建的全局注册 Symbol 不能被弱持有,因为注册表本身长期持有它。
这是底层元数据和能力令牌场景的增强,普通业务使用对象作为 WeakMap 键仍更常见。
十一、ES2024:分组、Promise 构造与二进制内存增强
11.1 Object.groupBy
它按回调返回的属性键对可迭代数据分组:
js
const orders = [
{ id: 1, status: 'paid', amount: 100 },
{ id: 2, status: 'pending', amount: 80 },
{ id: 3, status: 'paid', amount: 120 },
]
const grouped = Object.groupBy(
orders,
(order) => order.status,
)
console.log(grouped.paid)
// id 为 1、3 的订单
结果是 null 原型对象,避免 __proto__ 等继承属性冲突:
js
Object.getPrototypeOf(grouped) === null // true
回调返回值会转换成属性键,所以对象键通常变成字符串 "[object Object]";需要对象身份作为键时使用 Map.groupBy。
工作落点
- 表格按状态、日期或部门分组。
- 报表数据预处理。
- UI 分区列表和批量操作队列。
分组后的元素仍是原对象引用:
js
grouped.paid[0].amount = 999
console.log(orders[0].amount) // 999
groupBy 只创建分组容器,不复制元素。
11.2 Map.groupBy
js
const paidGroup = { name: '已支付' }
const pendingGroup = { name: '待支付' }
const groupedByObject = Map.groupBy(orders, (order) =>
order.status === 'paid' ? paidGroup : pendingGroup,
)
groupedByObject.get(paidGroup)
Map 保留键的类型和身份,适合按对象、DOM 节点、实例或非字符串值分组。
选择原则:
text
结果要按字符串/Symbol 属性读取 -> Object.groupBy
结果键是任意值或对象身份 -> Map.groupBy
11.3 Promise.withResolvers
过去需要在 Promise 构造器外捕获 resolve/reject:
js
let resolveTask
let rejectTask
const task = new Promise((resolve, reject) => {
resolveTask = resolve
rejectTask = reject
})
现在可以直接得到三元组:
js
const {
promise,
resolve,
reject,
} = Promise.withResolvers()
实际场景:把事件转成 Promise
js
function waitForMessage(target, type) {
const { promise, resolve, reject } = Promise.withResolvers()
const controller = new AbortController()
target.addEventListener(type, resolve, {
once: true,
signal: controller.signal,
})
target.addEventListener('error', reject, {
once: true,
signal: controller.signal,
})
return {
promise: promise.finally(() => controller.abort()),
cancel: () => {
controller.abort()
reject(new DOMException('已取消', 'AbortError'))
},
}
}
适合事件桥接、队列、锁、测试控制和底层异步原语。普通请求流程仍优先直接返回已有 Promise;把 resolve/reject 暴露到大范围共享状态会让状态变化难以追踪。
11.4 良构 Unicode 字符串
JavaScript 字符串由 UTF-16 code unit 组成,可能包含孤立代理项。它们在某些编码、URL 和系统边界会产生问题。
js
const malformed = '\uD800'
malformed.isWellFormed() // false
malformed.toWellFormed() // '\uFFFD',替换为替代字符
正常代理对保持不变:
js
'😀'.isWellFormed() // true
工作落点
- 把不可信文本交给 UTF-8 编码器、文件系统或外部 API 前检查。
- 日志、爬虫、协议网关和数据清洗。
- 富文本编辑器处理截断字符串时修复非法序列。
toWellFormed 会丢失无法合法解释的原 code unit,因此关键取证数据不应未经记录直接覆盖原文。
11.5 RegExp /v 标志
v 是增强的 Unicode sets 模式,支持更强的字符集合运算和字符串属性。它与 u 互斥。
集合交集:
js
const greekLetters = /[\p{Script=Greek}&&\p{Letter}]/v
greekLetters.test('α') // true
greekLetters.test('A') // false
集合差集:
js
const nonAsciiDigits = /[\p{Decimal_Number}--[0-9]]/v
它适合国际化校验、编辑器、词法分析和 Unicode 数据处理。用户名等业务字段不能只用"Unicode Letter"粗略放行,应根据安全、归一化和产品规则建立白名单。
11.6 可调整大小和可转移 ArrayBuffer
Resizable ArrayBuffer
js
const buffer = new ArrayBuffer(8, {
maxByteLength: 1024,
})
buffer.resizable // true
buffer.maxByteLength // 1024
buffer.resize(64)
buffer.byteLength // 64
长度跟踪视图和固定长度视图在 resize 后行为不同,底层库必须明确视图创建方式和越界语义。
ArrayBuffer transfer
js
const source = new ArrayBuffer(1024)
const moved = source.transfer()
source.detached // true
moved.byteLength // 1024
transfer 把底层数据所有权移动到新缓冲区,原缓冲区被分离。适合 Worker 通信、文件处理、解析管线中避免大块内存复制。
js
buffer.transferToFixedLength()
可把可调整缓冲区转成固定长度缓冲区。
这些是性能和底层数据处理能力。普通 JSON 业务没有必要引入 ArrayBuffer 的生命周期复杂度。
Growable SharedArrayBuffer
SharedArrayBuffer 可以声明最大长度并在需要时增长:
js
const shared = new SharedArrayBuffer(16, {
maxByteLength: 1024,
})
shared.growable // true
shared.maxByteLength // 1024
shared.grow(64)
它只能增长,不能缩小,这避免其他执行代理看到共享内存突然被截断。适合 Worker、WASM 和并行运行时动态扩展共享区;仍需要 Atomics 和明确的并发协议。
11.7 Atomics.waitAsync
Atomics.wait 会阻塞执行代理,主线程通常不能使用;waitAsync 返回异步等待结果:
js
const result = Atomics.waitAsync(
int32Array,
index,
expectedValue,
timeout,
)
if (result.async) {
const status = await result.value
console.log(status) // ok、not-equal 或 timed-out
} else {
console.log(result.value)
}
适合共享内存并发协调、Worker 运行时和 WebAssembly。它要求对原子操作和内存模型有充分理解,不是普通异步等待的替代品。
十一补充、ES2024 的工程选择
text
数组按字符串键分组 Object.groupBy
数组按对象身份分组 Map.groupBy
需要外部控制 Promise 状态 Promise.withResolvers
检查 UTF-16 数据边界 isWellFormed/toWellFormed
复杂 Unicode 字符集合 RegExp /v
大块二进制内存扩容与移交 Resizable/Transferable ArrayBuffer
共享内存异步同步 Atomics.waitAsync
这些 API 的抽象层级差异很大。分组和 Promise 辅助适合日常业务;共享内存和缓冲区迁移主要属于框架、Worker、音视频、WASM 和底层工具。
十二、ES2025:Iterator 流水线、Set 运算与模块属性
12.1 Iterator Helpers
ES2025 引入全局 Iterator 及一组迭代器辅助方法,让惰性数据处理不必先转换成数组。
创建 Iterator
js
const iterator = Iterator.from([1, 2, 3, 4])
惰性转换方法
js
const result = Iterator.from([1, 2, 3, 4, 5])
.filter((value) => value % 2 === 1)
.map((value) => value * 10)
.take(2)
.toArray()
console.log(result) // [10, 30]
map、filter、take、drop、flatMap 返回新的 helper iterator,只有消费时才逐项计算:
js
function* infiniteIds() {
let id = 1
while (true) yield id++
}
const firstThreeEvenIds = infiniteIds()
.filter((id) => id % 2 === 0)
.take(3)
.toArray()
// [2, 4, 6]
无限序列如果先转数组会永远无法完成;惰性 take 能在满足数量后关闭上游迭代器。
终结方法
js
Iterator.from(values).reduce((sum, value) => sum + value, 0)
Iterator.from(values).some((value) => value > 100)
Iterator.from(values).every((value) => value >= 0)
Iterator.from(values).find((value) => value.id === id)
Iterator.from(values).forEach(handleValue)
Iterator.from(values).toArray()
工作落点
- 大数据过滤管线,减少中间数组。
- 文件记录、分页游标、树遍历和生成器结果。
- 无限序列和只需消费前 N 项的任务。
Iterator 通常一次性消费,重复使用同一个迭代器可能得到空结果。Iterator Helpers 是同步能力;异步 Iterator Helpers 在当前正式标准主线中仍要另行确认,不应混写。
12.2 Set 集合运算
并集、交集和差集
js
const frontend = new Set(['Alice', 'Bob'])
const backend = new Set(['Bob', 'Carol'])
frontend.union(backend)
// Alice、Bob、Carol
frontend.intersection(backend)
// Bob
frontend.difference(backend)
// Alice
frontend.symmetricDifference(backend)
// Alice、Carol
关系判断
js
const required = new Set(['read', 'write'])
const actual = new Set(['read', 'write', 'delete'])
required.isSubsetOf(actual) // true
actual.isSupersetOf(required) // true
required.isDisjointFrom(new Set(['admin'])) // true
工作落点
- 权限校验。
- 标签筛选和多选状态。
- 缓存键失效范围。
- 推荐系统中的集合关系。
js
function hasAllPermissions(userPermissions, requiredPermissions) {
return new Set(requiredPermissions).isSubsetOf(
new Set(userPermissions),
)
}
Set 比数组 includes 循环更直接表达集合语义。对象元素仍按身份比较,内容相同的两个对象不是同一成员;业务实体集合通常使用 ID。
12.3 Import Attributes 与 JSON Modules
导入时可以声明模块类型属性:
js
import config from './config.json' with { type: 'json' }
动态导入:
js
const module = await import('./config.json', {
with: { type: 'json' },
})
属性让宿主明确知道模块应按什么类型加载和校验,避免仅依赖扩展名或服务端 MIME 推断。
工作落点与注意事项
- 构建时或运行时加载静态 JSON 配置、语言包和数据表。
- Node.js、浏览器和构建工具对路径、MIME、缓存和支持范围可能不同。
- JSON 模块适合可信静态资源,不替代运行时接口响应校验。
- 旧代码可能使用早期
assert { type: 'json' }语法;新标准使用with { type: 'json' },迁移时检查工具链。
12.4 RegExp.escape
动态构造正则时,用户文本中的特殊字符必须转义:
js
const keyword = 'price (C++)'
const pattern = new RegExp(RegExp.escape(keyword), 'gi')
pattern.test('PRICE (C++)') // true
不能只手写替换 .*+? 等字符,因为安全转义还涉及首字符、标点、控制字符、空白和 Unicode 等边界。
实际搜索高亮:
js
function createLiteralSearchPattern(keyword) {
return new RegExp(RegExp.escape(keyword), 'giu')
}
RegExp.escape 解决的是"把文本当字面量嵌入正则",不是输入安全的全部问题。超长或复杂正则仍可能带来性能风险;纯字面搜索可以直接用 includes、indexOf。
12.5 正则内联修饰符
可以只在模式局部启用或禁用 i、m、s 标志:
js
const pattern = /prefix-(?i:hello)-suffix/
pattern.test('prefix-HELLO-suffix') // true
也可以在局部关闭:
js
const pattern = /(?i:product)-(?-i:SKU)/
适合组合多个来源、不同大小写或多行规则的复杂模式,比拆成多个正则更容易保持一次匹配上下文。普通校验应优先简单正则,避免把业务解析写成无法维护的模式语言。
12.6 Promise.try
Promise.try 调用一个可能同步返回、同步抛错或返回 Promise 的函数,并统一得到 Promise:
js
function executePlugin(plugin, input) {
return Promise.try(plugin, input)
}
等价目标是把同步与异步实现统一:
js
Promise.try(() => parseConfig(raw))
.then(useConfig)
.catch(handleError)
如果 parseConfig 同步抛错,错误会变成 Promise 拒绝;如果返回 Promise,则采用其状态。
它与下面写法有时机差异:
js
Promise.resolve().then(() => parseConfig(raw))
后者把函数调用安排到微任务;Promise.try 在调用时执行函数,并把结果标准化为 Promise。
工作落点
- 插件系统允许同步或异步钩子。
- 缓存层可能同步命中,也可能异步读取。
- 测试运行器和任务管线统一错误处理。
如果函数契约本来就明确返回 Promise,没有必要额外包装。
12.7 Float16Array、DataView Float16 与 Math.f16round
半精度浮点数使用 16 位存储,精度和范围显著低于普通 Number:
js
const values = new Float16Array([1.5, 3.1415926])
console.log(values[0]) // 1.5
console.log(values[1]) // 会按 float16 舍入
DataView:
js
const buffer = new ArrayBuffer(2)
const view = new DataView(buffer)
view.setFloat16(0, 1.5, true)
view.getFloat16(0, true) // 1.5
模拟舍入:
js
Math.f16round(3.1415926)
工作落点
- WebGPU、机器学习模型、图形、音视频和大规模科学数据。
- 需要用精度换内存和带宽的二进制协议。
金额、业务计数和高精度计算不应使用 Float16。它的价值是紧凑表示,不是更精确。
12.8 重复命名捕获组
在互斥的正则分支中,可以复用同一个捕获组名称,让不同输入格式得到统一结果字段:
js
const datePattern = /(?:(?<year>\d{4})-\d{2}-\d{2}|\d{2}\/\d{2}\/(?<year>\d{4}))/
datePattern.exec('2026-08-13').groups.year // 2026
datePattern.exec('08/13/2026').groups.year // 2026
未参与匹配的同名分支值为 undefined,参与分支提供最终值。这个能力只允许不会在同一匹配路径产生歧义的模式;随意在同一分支重复名称仍是语法错误。
适合同时兼容多种日志、日期或协议格式,同时向后续处理暴露统一字段名。格式越来越多时应转向明确解析器,而不是无限扩大单个正则。
12.9 规范一致性修正
ES2025 还修正了非严格模式下,全局 eval 引入的 var 与既有声明之间的一些重声明规则,使不同实现行为一致。这类变化主要影响遗留脚本和引擎一致性测试。
现代项目应使用 ESM、严格模式并避免直接 eval。它不是值得主动采用的业务能力,但属于年度规范变更的一部分。
十三、ES2026:精确求和、异步集合与二进制互操作
ES2026 是第 17 版 ECMAScript,于 2026 年 6 月正式发布。以下能力已经属于年度正式标准,但部署到生产环境前仍需检查目标浏览器、Node.js 和构建运行时的实际实现状态。
13.1 Math.sumPrecise
普通累加会受浮点舍入和数值量级影响:
js
[1e20, 1, -1e20].reduce((sum, value) => sum + value, 0)
// 0,小的 1 在中间计算中丢失
Math.sumPrecise 对 Number 可迭代对象进行更稳定的求和,尽量减少不同量级数字带来的精度损失:
js
Math.sumPrecise([1e20, 1, -1e20]) // 1
它接收 iterable,不限于数组:
js
Math.sumPrecise(new Set([0.1, 0.2, 0.3]))
工作落点与边界
- 科学计算、统计聚合、数值分析和大量浮点数据汇总。
- 提高求和稳定性,但不会把二进制浮点变成十进制定点。
- 金额仍应使用最小货币单位、明确舍入规则或十进制方案。
- 输入必须是 Number;BigInt 需要单独的整数聚合。
13.2 Iterator.concat
它惰性地把多个可迭代对象按顺序连接:
js
const iterator = Iterator.concat(
[1, 2],
new Set([3, 4]),
function* () {
yield 5
yield 6
}(),
)
iterator.toArray() // [1, 2, 3, 4, 5, 6]
与数组展开对比:
js
const eager = [...first, ...second, ...third]
数组展开会立即消费并创建完整数组;Iterator.concat 在下游消费时才逐个进入各来源,适合大型数据、生成器和无限/有限流组合。
js
const firstTen = Iterator.concat(sourceA, sourceB)
.filter(isValid)
.take(10)
.toArray()
如果前一个来源是无限迭代器,后面的来源永远不会被访问,这是顺序拼接的必然语义。
13.3 Array.fromAsync
它从异步可迭代对象、同步可迭代对象或类数组异步创建数组:
js
async function* loadPages() {
yield 1
yield Promise.resolve(2)
yield 3
}
const values = await Array.fromAsync(loadPages())
// [1, 2, 3]
带异步 mapper:
js
const users = await Array.fromAsync(
userIds,
async (id) => fetchUser(id),
)
与 Promise.all(Array.from(...)) 的差别
Array.fromAsync 面向异步迭代和逐项等待,通常按来源顺序消费,并不等价于把全部映射任务一次性并行发出。
js
// 倾向串行消费/映射语义
await Array.fromAsync(ids, fetchUser)
// 明确全部并行启动
await Promise.all(Array.from(ids, fetchUser))
工作落点
- 把异步生成器、流式分页结果收集成数组。
- 在测试或数据量可控时消费 Web/Node 异步数据源。
- 数据量很大或无限流时不要收集全部,直接
for await...of逐项处理,避免内存增长。
13.4 Error.isError
js
Error.isError(value)
它用于可靠判断 Error 对象,包括跨 Realm 场景:
js
const iframeError = iframe.contentWindow.eval(
'new Error("failed")',
)
iframeError instanceof Error // 可能是 false
Error.isError(iframeError) // true
instanceof 依赖当前 Realm 的 Error.prototype 是否出现在对象原型链上;Error.isError 使用语言内部品牌判断,更适合 iframe、Worker 边界、插件沙箱和跨上下文日志系统。
普通对象 { name: 'Error', message: 'x' } 不会因此成为真正 Error。外部 JSON 传来的错误仍要按数据结构解析。
13.5 Map 与 WeakMap 的 get-or-insert
直接默认值
js
const cache = new Map()
const list = cache.getOrInsert('users', [])
list.push('Alice')
键已存在时返回现有值;不存在时插入给定默认值并返回。
延迟计算默认值
js
const permissions = cache.getOrInsertComputed(
userId,
(key) => loadDefaultPermissions(key),
)
回调只在键缺失时执行,适合创建成本较高的默认对象:
js
function groupByKey(map, key, item) {
map.getOrInsertComputed(key, () => []).push(item)
}
WeakMap 也提供对应方法,适合实例元数据:
js
const metadata = new WeakMap()
const meta = metadata.getOrInsertComputed(
element,
() => ({ clicks: 0 }),
)
注意事项
getOrInsert(key, expensive())会先计算参数,即使键已存在;昂贵默认值要用getOrInsertComputed。- 回调可能修改同一个 Map,规范定义了相应结果,但业务代码应避免重入带来的复杂性。
- 存入可变数组或对象后,后续调用拿到的是同一个值,这是缓存的目的,不是自动复制。
13.6 Uint8Array Base64 与 Hex 转换
Base64
js
const bytes = new Uint8Array([
72, 101, 108, 108, 111,
])
bytes.toBase64() // SGVsbG8=
Uint8Array.fromBase64('SGVsbG8=')
// Uint8Array [72, 101, 108, 108, 111]
URL 安全字母表和省略填充:
js
bytes.toBase64({
alphabet: 'base64url',
omitPadding: true,
})
Hex
js
bytes.toHex() // 48656c6c6f
Uint8Array.fromHex('48656c6c6f')
写入已有缓冲区
js
const target = new Uint8Array(16)
const result = target.setFromBase64('SGVsbG8=')
console.log(result.read)
console.log(result.written)
setFromHex 提供对应能力。
工作落点
- 文件摘要、加密材料、WebSocket 和二进制协议。
- JWT 片段、URL 安全编码、图片和附件数据。
- 浏览器与 Node.js 通用库减少对
atob、btoa、Buffer 和手写循环的环境分支。
Base64 只是编码,不是加密;数据体积通常还会膨胀,二进制上传优先使用 Blob、ArrayBuffer 或 multipart,而不是无条件塞进 JSON。
13.7 JSON.parse Source Text Access
reviver 获得额外 context,可访问原始 JSON 文本片段:
js
const json = '{"id":900719925474099312345}'
const data = JSON.parse(json, (key, value, context) => {
if (key === 'id') {
return BigInt(context.source)
}
return value
})
console.log(data.id)
// 900719925474099312345n
为什么不能用 BigInt(value)?因为 JSON 数字转成 Number 时可能已经丢失精度,而 context.source 保留匹配到的原始数字文本。
context 只在对应值仍与原始 JSON 节点相关时提供 source;对象和数组的处理语义与原始值不同,实际 reviver 要按规范和运行环境测试。
工作落点
- 大整数 ID、金融或科学数据的精确恢复。
- 需要保留数字原始格式的解析器。
- 审计工具比较原始文本与解析值。
最稳妥的跨系统契约通常仍是把超长整数作为 JSON 字符串传输,减少旧运行时和其他语言实现差异。
13.8 JSON.rawJSON 与 JSON.isRawJSON
JSON.rawJSON 创建一个受控包装,JSON.stringify 会把其中合法的原始 JSON 原始值文本直接写入结果:
js
const payload = {
id: JSON.rawJSON('900719925474099312345'),
}
JSON.stringify(payload)
// {"id":900719925474099312345}
检查:
js
JSON.isRawJSON(payload.id) // true
输入必须是合法的 JSON 原始值文本,外层不能是对象或数组,非法文本会抛 SyntaxError:
js
JSON.rawJSON('true')
JSON.rawJSON('null')
JSON.rawJSON('123.45')
// JSON.rawJSON('{"x":1}') // SyntaxError
工作落点与风险
- 精确输出超过安全整数范围的 JSON 数字。
- 底层序列化库控制特定原始值表示。
- 与
JSON.parsesource context 配合完成大整数往返。
不要把未经校验的用户字符串直接当 raw JSON。该 API 会验证 JSON 语法,但业务仍需验证值范围和类型。大部分接口继续使用普通 JSON.stringify 即可。
十四、按工作场景选择现代特性
按年份背 API 适合建立历史地图,写代码时更需要按问题选择工具。
14.1 配置与默认值
js
function normalizeOptions(input = {}) {
return {
timeout: input.timeout ?? 5000,
retry: input.retry ?? 0,
enabled: input.enabled ?? true,
}
}
推荐组合:默认参数、对象解构、对象展开、??、??=、可选链。
关键区别:
text
|| / ||= 把所有假值视为缺失
?? / ??= 只把 null、undefined 视为缺失
14.2 列表查询、变换与状态更新
js
const visible = users
.filter((user) => user.enabled)
.toSorted((a, b) => a.name.localeCompare(b.name))
const nextUsers = users.with(index, {
...users[index],
enabled: false,
})
推荐组合:find、findLast、includes、flatMap、toSorted、toSpliced、with。
数据规模大、来源是生成器或只消费前几项时,考虑 Iterator Helpers,避免中间数组。
14.3 索引、分组与集合关系
js
const userById = new Map(
users.map((user) => [user.id, user]),
)
const usersByDepartment = Object.groupBy(
users,
(user) => user.department,
)
const allowed = requiredPermissions.isSubsetOf(userPermissions)
text
固定字段实体 Object
动态任意键索引 Map
唯一成员集合 Set
字符串键分组 Object.groupBy
对象身份键分组 Map.groupBy
对象生命周期缓存 WeakMap
14.4 异步任务选择
text
全部成功才成功 Promise.all
等待全部并保留每项结果 Promise.allSettled
第一个完成,不论成败 Promise.race
第一个成功 Promise.any
同步/异步函数统一包装 Promise.try
外部控制 Promise Promise.withResolvers
顺序异步流 for await...of
异步流收集为数组 Array.fromAsync
任何聚合方法都不会自动取消任务。网络请求取消、超时和资源释放需要 AbortController 或宿主提供的取消协议。
14.5 模块与按需加载
text
稳定静态依赖 import/export
低频重型功能 dynamic import()
模块级启动依赖 top-level await(谨慎)
静态 JSON 资源 import attributes + JSON module
模块旁资源定位 import.meta.url
模块边界应按职责设计,不能把动态导入当成修复循环依赖的工具。
14.6 文本、正则与 Unicode
js
const escaped = RegExp.escape(keyword)
const pattern = new RegExp(escaped, 'giu')
if (!text.isWellFormed()) {
text = text.toWellFormed()
}
纯文本查找优先 includes、startsWith、replaceAll;动态正则字面文本使用 RegExp.escape;国际化字符集合使用 Unicode 属性和 /v。HTML、URL、JSON 和编程语言应使用结构化解析器。
14.7 二进制与高精度数据
text
超长整数运算 BigInt
超长业务 ID 通常用 String
更稳定的浮点求和 Math.sumPrecise
半精度紧凑数据 Float16Array
Base64/Hex 字节转换 Uint8Array 新 API
大块内存扩容/转移 ArrayBuffer resize/transfer
JSON 大整数恢复 reviver context.source
JSON 原始数字输出 JSON.rawJSON
"精确"要先定义业务含义:BigInt 只处理整数;Math.sumPrecise 降低浮点求和误差但不是十进制金额模型;Float16 反而主动牺牲精度。
十五、兼容性与工程落地
15.1 不要用"支持 ES6"描述兼容范围
一个运行环境可能支持箭头函数,却不支持 Proxy;可能支持可选链,却还没有 ES2025 Iterator Helpers。正确做法是按特性和目标版本判断。
项目至少要明确:
text
浏览器:支持哪些版本、是否包含 WebView、是否需要旧 Safari
Node.js:最低主版本与模块模式
构建工具:语法转换目标与模块输出
运行时:是否注入 polyfill,按需还是全量
发布包:应用、库、Node 服务的兼容策略是否不同
应用代码可以根据自己的用户环境定制;发布 npm 库时要避免偷偷污染全局,并清楚声明构建产物、模块格式与最低运行环境。
15.2 browserslist 与目标环境
前端项目通常通过 browserslist 一类配置表达目标环境,Babel、Autoprefixer 和构建工具据此决定转换范围。
示意配置:
text
> 0.5%
last 2 versions
not dead
这只是示例,不应直接复制到所有项目。内部后台系统可能只支持公司统一浏览器,公共消费产品可能需要覆盖更多移动设备,嵌入式 WebView 还要按宿主实际版本测试。
目标越旧,通常意味着:
- 更多语法转换代码。
- 更多 polyfill 和包体积。
- 更复杂的 Source Map 与调试栈。
- 某些底层能力无法完全模拟。
兼容范围是产品决策,不是开发者凭习惯选择。
15.3 哪些能力可以转译
语法层能力通常能改写为更旧语法:
text
箭头函数
解构与默认参数
可选链与空值合并
类字段
async/await(通常需要辅助运行时代码)
对象展开
但转换后的代码不一定与原生实现成本相同。例如把 async/await 转成生成器和辅助函数会增加代码量;私有字段转换可能借助 WeakMap,性能、调试和反射表现与原生语义有差异。
15.4 哪些能力需要 polyfill
运行时 API 通常需要在旧环境提供实现:
text
Array.prototype.includes
Object.entries / Object.fromEntries
Promise.allSettled / Promise.any
Array.prototype.flat / toSorted
Object.groupBy
Set 新集合方法
Iterator Helpers
Polyfill 策略主要有两种:
- 全局注入:修改或补充全局构造器与原型,应用使用方便,但可能污染宿主环境。
- 纯函数/ponyfill:按模块导入实现,不修改全局,库更可控,但调用形式可能不同。
按需注入必须建立在可靠静态分析和目标环境数据上。动态属性访问、第三方依赖和运行时生成代码可能让自动检测遗漏。
15.5 哪些能力难以完整 polyfill
text
Proxy
WeakRef / FinalizationRegistry
SharedArrayBuffer / Atomics
真正的私有字段语义
模块加载与 top-level await
ArrayBuffer transfer 的所有权转移
部分正则引擎语义
这类能力涉及引擎内部槽、垃圾回收、解析器、模块加载器或内存模型。工具可能提供受限替代或转换,但不能保证在所有可观察行为上等价。
使用前需要选择:提升最低运行环境、提供降级路径、服务端完成相关工作,或暂时不采用。
15.6 Feature Detection
检测 API 是否存在:
js
if (typeof Promise.withResolvers === 'function') {
// 使用原生能力
}
js
if ('toSorted' in Array.prototype) {
// ...
}
不能用 try/catch 在不支持的引擎中动态检测某些新语法,因为源码在执行前解析就失败。语法兼容应由构建目标、模块分发或单独脚本入口解决。
Feature Detection 能判断"是否存在",不保证早期实现没有缺陷,也不替代真实设备测试。
15.7 应用和库采用新特性的不同策略
应用
- 掌握用户运行环境,可以统一构建和注入 polyfill。
- 新特性带来的可读性收益通常可以优先享受。
- 仍要监控真实用户浏览器和 WebView 版本。
库
- 不知道调用方环境和构建链,不能假设全局 polyfill 一定存在。
- 应在
package.json、文档和发布说明中声明最低环境。 - 同时发布 ESM/CJS 或不同语法目标时,要确保
exports映射清晰。 - 尽量不修改全局原型,不把重量级 polyfill 隐式打进每个调用方。
15.8 性能不是"新语法一定更快"
语言特性的首要价值通常是语义和可靠性。性能需要按数据量和实现测量:
- Iterator Helpers 能减少中间数组,但小数组上差异可能无意义。
- Map 查找适合动态大索引,固定小对象不必全部替换。
toSorted提供不可变语义,也必然创建新数组。- 动态 import 减少初始包,但把下载等待转移到功能首次使用时。
- Float16 节省内存,但精度降低且转换有成本。
先确定问题,再测量瓶颈,最后选择特性。不要把 API 年份当性能等级。
十六、常见误区集中说明
1. const 等于对象不可变
错误。它只禁止重新赋值绑定,嵌套属性仍可修改。
2. 箭头函数更现代,所以所有函数都用箭头
错误。动态 this、构造调用、生成器和原型共享方法仍需要其他函数形式。
3. 展开语法等于深拷贝
错误。数组和对象展开都只处理一层,嵌套对象继续共享引用。
4. async/await 会让任务并行
错误。它表达等待;是否并行取决于任务何时启动。连续 await 通常是串行。
5. Promise 能取消
Promise 本身表示结果,没有统一取消协议。取消依赖任务 API,例如 fetch 的 AbortSignal。
6. 可选链越多越健壮
错误。它适合合法缺失值;对系统必须存在的数据滥用,会把清晰错误变成远处的 undefined。
7. || 和 ?? 可以随便替换
错误。|| 处理所有假值,?? 只处理空值。0、空字符串、false 是否有效由业务决定。
8. Map 一定比 Object 快
错误。性能依赖引擎、键类型、数据规模和操作模式。先按数据语义选择,再在热点测量。
9. WeakMap 是自动过期缓存
不完整。它只在键对象不可达时允许回收,没有 TTL、容量限制和可枚举能力。
10. Babel 可以解决全部兼容问题
错误。解析和语法转换、运行时 API、引擎内部语义是不同层次。
11. Stage 3 就能放心写生产
不一定。设计通常较稳定,但仍不是正式完成;环境支持、工具链和未来变化风险要单独评估。
12. 年度标准发布后所有浏览器立即支持
错误。标准、引擎实现、浏览器发版、企业更新和 WebView 升级之间存在时间差。
十七、ES2027 候选与未来提案怎么判断
截至 2026 年 8 月,ES2026 是最近正式年度版本。TC39 已有一些 Stage 4 完成提案预计进入 ES2027,但它们还不属于 ES2026 主线。
17.1 Temporal
Temporal 旨在提供比 Date 更清晰的日期、时间、时区和日历模型,区分时间点、纯日期、带时区日期时间和持续时间等概念。
示意:
js
const date = Temporal.PlainDate.from('2026-08-13')
const nextWeek = date.add({ days: 7 })
真实落点包括预约、跨时区会议、账期和日历计算。进入生产前要检查运行环境,或使用官方/成熟 polyfill 并评估包体积。
17.2 显式资源管理
通过 using、await using、Symbol.dispose、Symbol.asyncDispose 等机制,让资源在离开作用域时可靠释放:
js
class Subscription {
[Symbol.dispose]() {
this.unsubscribe()
}
}
// 在支持该语法的环境中:
// using subscription = createSubscription()
它适合文件句柄、锁、订阅和连接,不替代垃圾回收。
17.3 Joint Iteration 与 Atomics.pause
Joint Iteration 面向多个可迭代序列的联合推进;Atomics.pause 为自旋等待等底层并发循环提供优化提示。这两类能力的使用人群和风险不同,前者偏数据处理,后者偏运行时与并发库。
17.4 Decorators、Pipeline、Async Iterator Helpers
这些能力长期受到关注,但截至本文时间点不能仅因为 TypeScript、Babel 或某些浏览器支持就宣称已经进入 ES2026。
- Decorators 仍需关注最终标准语义与旧版 TypeScript 装饰器差异。
- Pipeline Operator 的语法和设计仍处于提案过程。
- Async Iterator Helpers 不应和 ES2025 已正式完成的同步 Iterator Helpers 混为一谈。
提案采用原则
text
Stage 0/1:了解方向,不绑定生产架构
Stage 2:可以实验,避免对外公共 API 强依赖
Stage 2.7/3:规范较稳定,仍需工具和风险评估
Stage 4:等待/确认年度归属及目标环境实现
正式年度版:仍要检查运行环境兼容性
十八、年度速查表
text
ES2015 let/const、箭头、解构、模板、class、Symbol、Iterator、
Generator、Promise、ESM、Map/Set、Proxy/Reflect、大量内置 API
ES2016 指数运算符、Array.includes
ES2017 async/await、Object.values/entries、字符串填充、
属性描述符、函数尾逗号、SharedArrayBuffer/Atomics
ES2018 异步迭代、对象 rest/spread、Promise.finally、
正则 dotAll/命名组/后行断言/Unicode 属性
ES2019 flat/flatMap、Object.fromEntries、可选 catch、trimStart/End、
Symbol.description、稳定 sort、JSON 与函数源码改进
ES2020 可选链、空值合并、BigInt、动态 import、allSettled、
globalThis、matchAll、import.meta、模块命名空间转导出
ES2021 逻辑赋值、replaceAll、Promise.any/AggregateError、
数字分隔符、WeakRef/FinalizationRegistry
ES2022 类字段与私有元素、静态块、顶层 await、at、hasOwn、
Error cause、RegExp 匹配索引
ES2023 findLast/findLastIndex、toSorted/toReversed/toSpliced/with、
Hashbang、Weak 集合 Symbol 键
ES2024 Object/Map.groupBy、Promise.withResolvers、良构字符串、
RegExp /v、可调整/可转移 ArrayBuffer、可增长 SharedArrayBuffer、
Atomics.waitAsync
ES2025 Iterator Helpers、Set 运算、Import Attributes/JSON Modules、
RegExp.escape、正则局部修饰符、Promise.try、Float16、
重复命名捕获组及规范一致性修正
ES2026 Math.sumPrecise、Iterator.concat、Array.fromAsync、
Error.isError、Map/WeakMap get-or-insert、Uint8Array Base64/Hex、
JSON.parse source context、JSON.rawJSON
十九、学习与迁移顺序
如果项目仍主要使用 ES5 风格,可以按以下顺序迁移:
- 先掌握
let/const、函数、解构、模板字符串和对象简写。 - 用数组方法、Map、Set 表达正确的数据结构。
- 掌握 Promise、
async/await、并行与错误传播。 - 用 ESM 建立模块边界,理解静态与动态导入。
- 使用可选链、空值合并和非破坏性数组方法简化业务状态。
- 根据真实场景学习分组、Iterator Helpers、Set 运算和新 Promise API。
- 只有处理二进制、Worker、WASM 或底层框架时,再深入 SharedArrayBuffer、Atomics、Float16 和 ArrayBuffer transfer。
- 新项目建立兼容目标与自动化测试,不凭记忆判断环境支持。
迁移不需要一次改完全部旧代码。优先改动有测试覆盖、正在开发和容易出错的模块;纯格式变化不要与重大业务修改混在同一个提交中。
总结
从 ES2015 到 ES2026,JavaScript 的演进可以归纳为几条主线:
- 语法更准确地表达作用域、默认值、缺失值和不可变更新。
- Promise、async 函数和异步迭代逐步完善异步控制流。
- ESM、动态导入和模块属性让依赖关系更可分析。
- Map、Set、groupBy 和 Iterator Helpers 提供更贴近问题的数据抽象。
- class 私有元素、Proxy、WeakMap 等能力支撑框架和库封装。
- Unicode、正则、二进制数组和 JSON 精确处理不断补齐边界场景。
真正有价值的学习方式不是按年份背方法名,而是理解每项特性改变了哪条语义、解决了哪类重复代码、又引入了什么约束。工作中选择新特性时依次确认:它是否准确表达业务、团队是否能维护、目标环境是否支持、转换和 polyfill 成本是否可接受。