ES6 之后,ECMAScript 每年发布一个新版本。本文汇总 ES2016 到 ES2024 的主要新特性。
ES2016 (ES7)#
Array.prototype.includes#
// 之前用 indexOf;[1, 2, 3].indexOf(2) !== -1 // true
// ES2016;[1, 2, 3].includes(2) // true;[1, 2, 3].includes(4) // false
// includes 可以检测 NaN;[1, NaN, 3].indexOf(NaN) // -1(找不到);[1, NaN, 3].includes(NaN) // true
// 从指定位置开始搜索;[1, 2, 3].includes(1, 1) // false指数运算符 **#
// 之前Math.pow(2, 10) // 1024
// ES20162 ** 10 // 1024
// 右结合2 ** (3 ** 2) // 512(等于 2 ** 9)
// 与赋值结合let num = 2num **= 10 // 1024ES2017 (ES8)#
async/await#
async function fetchData() { const response = await fetch('/api/data') return response.json()}Object.values/Object.entries#
const obj = { a: 1, b: 2, c: 3 }
Object.values(obj) // [1, 2, 3]Object.entries(obj) // [['a', 1], ['b', 2], ['c', 3]]
// 实用场景for (const [key, value] of Object.entries(obj)) { console.log(`${key}: ${value}`)}String padding#
'5'.padStart(3, '0') // '005''5'.padEnd(3, '0') // '500'
// 格式化'123'.padStart(8, ' ') // ' 123'Object.getOwnPropertyDescriptors#
const obj = { get name() { return 'foo' },}
Object.getOwnPropertyDescriptors(obj)// {// name: {// get: [Function: get name],// set: undefined,// enumerable: true,// configurable: true// }// }
// 用于完整复制对象const clone = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))函数参数尾逗号#
function foo( param1, param2 // 允许尾逗号) {}
foo( 'a', 'b' // 允许尾逗号)SharedArrayBuffer 和 Atomics#
// 共享内存const sab = new SharedArrayBuffer(1024)const view = new Int32Array(sab)
// 原子操作Atomics.add(view, 0, 5)Atomics.load(view, 0)ES2018 (ES9)#
异步迭代#
async function* asyncGenerator() { yield await fetch('/api/1') yield await fetch('/api/2')}
for await (const response of asyncGenerator()) { console.log(response)}Promise.finally#
fetchData() .then((data) => console.log(data)) .catch((err) => console.error(err)) .finally(() => console.log('完成'))Rest/Spread 属性#
// Rest 属性const { a, ...rest } = { a: 1, b: 2, c: 3 }// a = 1, rest = { b: 2, c: 3 }
// Spread 属性const merged = { ...obj1, ...obj2 }正则表达式增强#
// 命名捕获组const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/const match = re.exec('2024-01-15')match.groups.year // '2024'match.groups.month // '01'match.groups.day // '15'
// 后行断言;/(?<=\$)\d+/.exec('$100') // ['100'];/(?<!\$)\d+/.exec('€100') // ['100']
// dotAll 模式;/foo.bar/s.test('foo\nbar') // true
// Unicode 属性转义;/\p{Script=Han}/u.test('中') // trueES2019 (ES10)#
Array.prototype.flat/flatMap#
;[1, [2, [3]]].flat() // [1, 2, [3]];[1, [2, [3]]].flat(2) // [1, 2, 3];[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
;[1, 2].flatMap((x) => [x, x * 2]) // [1, 2, 2, 4]Object.fromEntries#
const entries = [ ['a', 1], ['b', 2],]Object.fromEntries(entries) // { a: 1, b: 2 }
// Map 转对象const map = new Map([ ['a', 1], ['b', 2],])Object.fromEntries(map) // { a: 1, b: 2 }
// URLSearchParams 转对象const params = new URLSearchParams('a=1&b=2')Object.fromEntries(params) // { a: '1', b: '2' }String.prototype.trimStart/trimEnd#
' hello '.trimStart() // 'hello '' hello '.trimEnd() // ' hello'可选的 catch 绑定#
// 之前try { // ...} catch (error) { // 必须声明 error}
// ES2019try { JSON.parse(input)} catch { // 不需要 error 参数 console.log('解析失败')}Symbol.prototype.description#
const sym = Symbol('描述')sym.description // '描述'Function.prototype.toString 修订#
function /* 注释 */ foo() {}foo.toString() // 'function /* 注释 */ foo() {}'// 现在保留注释和空白ES2020 (ES11)#
可选链 ?.#
const user = { profile: { name: '张三' } }
user?.profile?.name // '张三'user?.address?.city // undefined(不报错)
// 方法调用obj.method?.()
// 数组访问arr?.[0]空值合并 ??#
const value = null ?? 'default' // 'default'const value2 = 0 ?? 'default' // 0
// 与 || 的区别0 || 'default' // 'default'0 ?? 'default' // 0
'' || 'default' // 'default''' ?? 'default' // ''BigInt#
const big = 9007199254740993nconst big2 = BigInt('9007199254740993')
big + 1n // 9007199254740994n
// 不能与普通数字混合运算// big + 1; // TypeErrorPromise.allSettled#
const results = await Promise.allSettled([ Promise.resolve(1), Promise.reject(new Error('失败')), Promise.resolve(3),])
// [// { status: 'fulfilled', value: 1 },// { status: 'rejected', reason: Error },// { status: 'fulfilled', value: 3 }// ]globalThis#
// 统一的全局对象访问globalThis.setTimeout // 浏览器和 Node.js 都可用String.prototype.matchAll#
const str = 'test1test2test3'const matches = [...str.matchAll(/test(\d)/g)]// [['test1', '1'], ['test2', '2'], ['test3', '3']]动态 import()#
const module = await import('./module.js')export * as#
export * as utils from './utils.js'ES2021 (ES12)#
String.prototype.replaceAll#
'aabbcc'.replaceAll('b', 'x') // 'aaxxcc'
// 之前需要正则'aabbcc'.replace(/b/g, 'x') // 'aaxxcc'Promise.any#
const first = await Promise.any([ fetch('/api/1'), fetch('/api/2'), fetch('/api/3'),])// 返回第一个成功的结果逻辑赋值运算符#
// ||=a ||= b // a = a || b
// &&=a &&= b // a = a && b
// ??=a ??= b // a = a ?? b
// 实用场景user.name ??= 'Anonymous'WeakRef 和 FinalizationRegistry#
// 弱引用const ref = new WeakRef(targetObject)const obj = ref.deref() // 可能是 undefined
// 清理回调const registry = new FinalizationRegistry((heldValue) => { console.log('对象被回收:', heldValue)})registry.register(obj, 'some value')数字分隔符#
const billion = 1_000_000_000const bytes = 0xff_ff_ff_ffconst bits = 0b1010_0001ES2022 (ES13)#
类字段和私有成员#
class Person { name = 'default' // 公有字段 #age = 0 // 私有字段 static count = 0 // 静态字段 static #secret = 'x' // 私有静态字段
#privateMethod() {} // 私有方法 static #staticPrivate() {} // 私有静态方法}静态初始化块#
class Config { static data
static { // 复杂的静态初始化 this.data = loadConfig() }}顶层 await#
// 模块顶层可以使用 awaitconst data = await fetch('/api/config').then((r) => r.json())export { data }in 操作符检查私有字段#
class Foo { #brand static isFoo(obj) { return #brand in obj }}Array.prototype.at#
const arr = [1, 2, 3, 4, 5]arr.at(-1) // 5arr.at(-2) // 4arr.at(0) // 1
'hello'.at(-1) // 'o'Object.hasOwn#
const obj = { prop: 'value' }
Object.hasOwn(obj, 'prop') // true
// 替代 hasOwnPropertyobj.hasOwnProperty('prop') // true// hasOwn 更安全,不受原型链影响RegExp match indices#
const re = /a+(?<Z>z)?/dconst match = re.exec('aaaz')match.indices // [[0, 4], [3, 4]]match.indices.groups // { Z: [3, 4] }Error cause#
try { doSomething()} catch (error) { throw new Error('操作失败', { cause: error })}ES2023 (ES14)#
数组查找从后往前#
const arr = [1, 2, 3, 2, 1]
arr.findLast((x) => x > 1) // 2arr.findLastIndex((x) => x > 1) // 3数组不可变方法#
const arr = [3, 1, 2]
arr.toSorted() // [1, 2, 3],原数组不变arr.toReversed() // [2, 1, 3],原数组不变arr.toSpliced(1, 1, 'a') // [3, 'a', 2],原数组不变arr.with(1, 'x') // [3, 'x', 2],原数组不变
// 原数组保持不变console.log(arr) // [3, 1, 2]Hashbang 语法#
#!/usr/bin/env nodeconsole.log('Hello')Symbol.prototype 作为 WeakMap 键#
const weak = new WeakMap()const key = Symbol('key')weak.set(key, 'value')ES2024 (ES15)#
Promise.withResolvers#
const { promise, resolve, reject } = Promise.withResolvers()
// 等价于let resolve, rejectconst promise = new Promise((res, rej) => { resolve = res reject = rej})Object.groupBy / Map.groupBy#
const items = [ { type: 'fruit', name: 'apple' }, { type: 'fruit', name: 'banana' }, { type: 'vegetable', name: 'carrot' },]
Object.groupBy(items, (item) => item.type)// {// fruit: [{ type: 'fruit', name: 'apple' }, ...],// vegetable: [{ type: 'vegetable', name: 'carrot' }]// }
Map.groupBy(items, (item) => item.type)// Map { 'fruit' => [...], 'vegetable' => [...] }正则表达式 v 标志#
// 集合操作;/[\p{Script=Greek}&&\p{Letter}]/v // 交集;/[\p{Script=Greek}--[αβγ]]/v // 差集
// 字符串字面量;/[\q{abc|def}]/v.test('abc') // trueArrayBuffer 可调整大小#
const buffer = new ArrayBuffer(8, { maxByteLength: 16 })buffer.resize(12)buffer.byteLength // 12Atomics.waitAsync#
const sab = new SharedArrayBuffer(4)const view = new Int32Array(sab)
// 异步等待Atomics.waitAsync(view, 0, 0).value.then(() => { console.log('值已改变')})特性汇总表#
| 版本 | 年份 | 主要特性 | | ------ | ---- | ------------------------------- | --- | --- | | ES2016 | 2016 | includes, ** | | ES2017 | 2017 | async/await, Object.entries | | ES2018 | 2018 | 异步迭代, Rest/Spread, 正则增强 | | ES2019 | 2019 | flat/flatMap, fromEntries | | ES2020 | 2020 | ?., ??, BigInt, allSettled | | ES2021 | 2021 | replaceAll, Promise.any, | | = | | ES2022 | 2022 | 类私有成员, 顶层 await, at() | | ES2023 | 2023 | findLast, 不可变数组方法 | | ES2024 | 2024 | withResolvers, groupBy, v 标志 |
持续关注 ECMAScript 新特性,能让代码更简洁、更现代。