ES6 到 ES2026 全面进阶指南:新特性、原理、示例与工程落地

很多人把 let、箭头函数和 Promise 称为"ES6",又把可选链、async/await 甚至所有现代 JavaScript 都笼统地叫作 ES6。这样交流虽然方便,却容易混淆特性所属版本、运行环境支持程度和构建工具职责。

这篇文章从 ECMAScript 2015,也就是通常所说的 ES6 开始,一直讲到 ECMAScript 2026。每个重要特性都会回答五个问题:

  1. 它解决了什么问题?
  2. 基本语法怎么写?
  3. 运行规则是什么?
  4. 工作中落在哪里?
  5. 有哪些兼容性与使用误区?

文章只把已经进入正式年度标准的能力放进 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 是语言语法,网络请求是否可以取消取决于 fetchAbortController 等宿主能力。

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 letconst 与块级作用域

概念

var 只有函数作用域,并允许重复声明;letconst 引入块级作用域。块包括函数体、ifforwhile 以及单独的 {}

js 复制代码
if (true) {
  let message = 'inside'
  const version = 2015
}

// console.log(message) // ReferenceError

const 表示绑定不能重新指向另一个值,不表示对象内部不可修改:

js 复制代码
const user = { name: 'Alice' }
user.name = 'Bob' // 合法

// user = {} // TypeError

暂时性死区

letconst 的绑定在进入作用域时已经创建,但执行到声明语句前没有初始化,这段区域叫暂时性死区(TDZ):

js 复制代码
{
  // console.log(count) // ReferenceError
  let count = 1
}

不要简单记成"let 不提升"。更准确的是:绑定已创建,但初始化前不能读取。

循环中的独立绑定

letfor 循环中会为每轮迭代创建独立绑定:

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,不会因为 callapply 或对象方法调用而改变:

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

箭头函数还没有自己的 argumentssupernew.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.iteratorSymbol.toPrimitiveSymbol.toStringTagSymbol.hasInstance

Symbol 属性不会出现在 Object.keys 和普通 for...in 中,但可以通过 Object.getOwnPropertySymbolsReflect.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

静态 importexport 位于模块顶层,工具可以在执行前建立依赖图。导入的是 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 包括 getsethasdeletePropertyownKeysapplyconstruct

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.isNaNNumber.isFinite 不会先隐式转换,通常比全局同名函数更适合做类型明确的业务校验。

二进制和八进制字面量:

js 复制代码
0b1010 // 10
0o755  // 493

RegExp 的 uy 标志

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.valuesObject.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 值为 undefinedstrings.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 trimStarttrimEnd

js 复制代码
'  hello  '.trimStart() // 'hello  '
'  hello  '.trimEnd()   // '  hello'

trimLefttrimRight 通常作为兼容别名存在,新代码优先使用不依赖书写方向的 trimStarttrimEnd

工作中常见于文本输入和文件内容处理。用户名、密码等字段是否允许或保留空格要按业务规则决定,不应统一对全部字段 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)

当可选链左侧是 nullundefined 时,当前连续链短路并返回 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 空值合并 ??

?? 只在左侧是 nullundefined 时使用右侧默认值:

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 的 globalglobalThis 提供统一访问方式:

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.keysObject.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) // 未命名

0false 和空字符串都会触发 ||=,所以它适合"所有假值都代表缺失"的情况。

??=:左侧为空值时赋值

js 复制代码
const config = {
  retry: 0,
}

config.retry ??= 3
config.timeout ??= 5000

console.log(config) // { retry: 0, timeout: 5000 }

??= 只把 nullundefined 当作缺失,更适合表单数字、开关和配置默认值。

&&=:左侧为真值时赋值

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.anyAggregateError

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 findLastfindLastIndex

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

传统 reversesortsplice 会修改原数组,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]

mapfiltertakedropflatMap 返回新的 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 解决的是"把文本当字面量嵌入正则",不是输入安全的全部问题。超长或复杂正则仍可能带来性能风险;纯字面搜索可以直接用 includesindexOf

12.5 正则内联修饰符

可以只在模式局部启用或禁用 ims 标志:

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 通用库减少对 atobbtoa、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.rawJSONJSON.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.parse source 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,
})

推荐组合:findfindLastincludesflatMaptoSortedtoSplicedwith

数据规模大、来源是生成器或只消费前几项时,考虑 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()
}

纯文本查找优先 includesstartsWithreplaceAll;动态正则字面文本使用 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 策略主要有两种:

  1. 全局注入:修改或补充全局构造器与原型,应用使用方便,但可能污染宿主环境。
  2. 纯函数/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 显式资源管理

通过 usingawait usingSymbol.disposeSymbol.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 风格,可以按以下顺序迁移:

  1. 先掌握 let/const、函数、解构、模板字符串和对象简写。
  2. 用数组方法、Map、Set 表达正确的数据结构。
  3. 掌握 Promise、async/await、并行与错误传播。
  4. 用 ESM 建立模块边界,理解静态与动态导入。
  5. 使用可选链、空值合并和非破坏性数组方法简化业务状态。
  6. 根据真实场景学习分组、Iterator Helpers、Set 运算和新 Promise API。
  7. 只有处理二进制、Worker、WASM 或底层框架时,再深入 SharedArrayBuffer、Atomics、Float16 和 ArrayBuffer transfer。
  8. 新项目建立兼容目标与自动化测试,不凭记忆判断环境支持。

迁移不需要一次改完全部旧代码。优先改动有测试覆盖、正在开发和容易出错的模块;纯格式变化不要与重大业务修改混在同一个提交中。

总结

从 ES2015 到 ES2026,JavaScript 的演进可以归纳为几条主线:

  • 语法更准确地表达作用域、默认值、缺失值和不可变更新。
  • Promise、async 函数和异步迭代逐步完善异步控制流。
  • ESM、动态导入和模块属性让依赖关系更可分析。
  • Map、Set、groupBy 和 Iterator Helpers 提供更贴近问题的数据抽象。
  • class 私有元素、Proxy、WeakMap 等能力支撑框架和库封装。
  • Unicode、正则、二进制数组和 JSON 精确处理不断补齐边界场景。

真正有价值的学习方式不是按年份背方法名,而是理解每项特性改变了哪条语义、解决了哪类重复代码、又引入了什么约束。工作中选择新特性时依次确认:它是否准确表达业务、团队是否能维护、目标环境是否支持、转换和 polyfill 成本是否可接受。

参考资料

相关推荐
众人皆醒我独醉1 小时前
训练加速实战:Flash Attention、Gradient Checkpointing 与数据流水线
后端·面试·gpu
Loveyourself1 小时前
Claude Code Memory 总体系核心代码逐行解析
面试·agent
用户43523713814931 小时前
🚀 Vue3 + TypeScript 学习笔记:从泛型 T 到 Promise<T>,终于搞懂了 keyof 和 typeof
前端
跟着群主去吃肉1 小时前
Cinemachine的应用-第三人称视角
前端·unity3d·游戏开发
东方小月1 小时前
从零开发一个 Coding Agent(十):实现 CLI 参数解析与静态命令
前端·人工智能·开源
阿黎梨梨1 小时前
Next.js 全栈开发:从 SPA 的痛点到 SSR 的破局之道
前端·后端
沐道PHP1 小时前
CRMEB多店版diy装修位置偏移修复
前端
WZzz1 小时前
JavaScript 中 this 的「指向」完整教程
javascript
犀利的毛豆豆1 小时前
useCallback
前端