ES2020-ES2024 核心特性
Site Owner
发布于 2026-05-21
详解Optional Chaining与??操作符、WeakRef与FinalizationRegistry、Error.cause、Promise.withResolvers及Temporal API设计

ES2020-ES2024 核心特性
Optional Chaining 与 Nullish Coalescing(ES2020);WeakRef 与 FinalizationRegistry(ES2021);Error.cause、Object.hasOwn、Array.at()(ES2022);Array grouping(Object.groupBy)、Promise.withResolvers(ES2024);Temporal API 的设计理念(替代 Date)。
目录
ES2020 核心特性
1. Optional Chaining ?.
解决痛点:深层属性访问时的空值判断链
// ❌ 旧写法:繁琐
const city = user && user.address && user.address.city;
// ✅ Optional Chaining
const city = user?.address?.city;
// 方法调用
user?.getName?.(); // 方法不存在时返回 undefined,不报错
arr?.[0]?.toUpperCase(); // 动态属性 + 方法
// 函数调用
const result = maybeFunction?.();
// 短路:?. 左边为 null/undefined 时,整个表达式短路为 undefined
console.log(null?.a?.b?.c); // undefined(不是 TypeError)
// 注意:?. 不是对所有 falsy 值的保护
console.log(''?.length); // 0(不是 undefined,'' 不是 null/undefined)
console.log(0?.toString()); // "0"
2. Nullish Coalescing ??
解决痛点:区分 null/undefined 与其他 falsy 值(0, '', false)
// ❌ || 的问题:0 和 '' 被当作 falsy
const port = userConfig.port || 3000; // 用户设置 0 也会被替换为 3000!
// ✅ ?? 只在 null/undefined 时取默认值
const port = userConfig.port ?? 3000; // 用户设置 0,则 port = 0
const name = user.name ?? 'Anonymous'; // 只有 null/undefined 时用默认值
const count = data.count ?? 0;
// 与 ?. 组合:链式访问 + 默认值
const theme = config?.theme?.color ?? '#333';
// ??= 赋值运算符(ES2021)
let x = null;
x ??= 'default'; // 仅在 null/undefined 时赋值
console.log(x); // 'default'
let y = 0;
y ??= 'default';
console.log(y); // 0(不是 null/undefined,不赋值)
3. Promise.allSettled
// Promise.all 有一个拒绝就整体失败
// Promise.allSettled 等待所有,不管结果
const promises = [
fetch('/api/users'),
fetch('/api/invalid-endpoint'), // 会失败
fetch('/api/products'),
];
const results = await Promise.allSettled(promises);
// results[i].status === 'fulfilled' → results[i].value
// results[i].status === 'rejected' → results[i].reason
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${i} 成功:`, result.value);
} else {
console.warn(`请求 ${i} 失败:`, result.reason.message);
}
});
4. BigInt
// 超过 Number.MAX_SAFE_INTEGER 的整数精确运算
const big = 9007199254740993n; // BigInt 字面量
const max = BigInt(Number.MAX_SAFE_INTEGER);
console.log(max + 1n); // 9007199254740992n(精确)
console.log(max + 1); // 9007199254740992(不精确!)
// 不能与 Number 混用
// big + 1; // TypeError
big + 1n; // ✅
// 转换
Number(42n); // 42
String(42n); // "42"
42n.toString(16); // "2a"(十六进制)
5. globalThis
// 统一访问全局对象,无论环境
console.log(globalThis); // 浏览器:Window;Node.js:global;Worker:DedicatedWorkerGlobalScope
// 旧方式需要判断环境:
const global = typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global
: typeof self !== 'undefined' ? self
: this;
// ✅ ES2020 统一方案
globalThis.myLibrary = { version: '1.0' };
ES2021 核心特性
1. WeakRef
解决痛点:需要持有对象引用,但不阻止 GC 回收
class Cache {
#store = new Map();
set(key, value) {
this.#store.set(key, new WeakRef(value));
}
get(key) {
const ref = this.#store.get(key);
if (!ref) return undefined;
const value = ref.deref(); // 尝试解引用
if (value === undefined) {
// 对象已被 GC 回收
this.#store.delete(key);
return undefined;
}
return value;
}
}
// 使用场景:DOM 节点缓存(节点被移除后自动失效)
const domCache = new Map();
function cacheElement(id, el) {
domCache.set(id, new WeakRef(el));
}
function getElement(id) {
return domCache.get(id)?.deref();
}
// ⚠️ 重要注意事项:
// 1. WeakRef.deref() 返回值可能在任意时刻变为 undefined(GC 随时可能运行)
// 2. 不要在 WeakRef.deref() 和使用之间让控制权回到事件循环
// 3. 同一次任务内,如果 deref() 返回了对象,它在这次任务结束前不会被回收
const ref = new WeakRef({ data: 'important' });
const obj = ref.deref();
if (obj) {
// obj 在这个同步代码块内是安全的
processData(obj.data);
}
2. FinalizationRegistry
// GC 回收对象时执行清理回调
const registry = new FinalizationRegistry((heldValue) => {
console.log(`对象 "${heldValue}" 已被回收`);
// 清理资源(如关闭文件句柄、取消订阅等)
});
let bigObject = { data: new ArrayBuffer(1024 * 1024 * 100) }; // 100MB
registry.register(bigObject, 'big-object-100mb'); // 注册,传入标识值
bigObject = null; // 允许 GC 回收
// 某个时刻后(GC 运行),控制台输出:对象 "big-object-100mb" 已被回收
// ⚠️ 注意:回调时机不确定,不能用于关键业务逻辑!
3. 逻辑赋值运算符
// &&= 仅在左侧为 truthy 时赋值
let a = 1;
a &&= 2; // a = 2(1 是 truthy)
let b = 0;
b &&= 2; // b = 0(0 是 falsy,不赋值)
// ||= 仅在左侧为 falsy 时赋值
let c = null;
c ||= 'default'; // c = 'default'
let d = 'existing';
d ||= 'default'; // d = 'existing'(不赋值)
// ??= 仅在左侧为 null/undefined 时赋值(前文已介绍)
// 实际应用:懒惰初始化
obj.cache ??= {};
obj.cache.key ||= computeExpensiveValue();
4. String.prototype.replaceAll
// 旧:需要全局正则
'a.b.c'.replace(/\./g, '-'); // "a-b-c"
// 新:直接字符串替换
'a.b.c'.replaceAll('.', '-'); // "a-b-c"
// 支持替换函数
'hello world'.replaceAll('o', (match, offset) => `[${offset}]`);
// "hell[4] w[7]rld"
ES2022 核心特性
1. Error.cause(链式错误)
// 解决"吞错误"问题:重新包装错误时保留原始原因
async function fetchUser(id) {
try {
const resp = await fetch(`/api/users/${id}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
} catch (e) {
// ✅ 保留原始错误作为 cause
throw new Error(`获取用户 ${id} 失败`, { cause: e });
}
}
// 调用方可以访问整个错误链
try {
await fetchUser(42);
} catch (e) {
console.error(e.message); // "获取用户 42 失败"
console.error(e.cause?.message); // "HTTP 404"
console.error(e.cause?.cause); // 更深的原因
}
2. Object.hasOwn(替代 hasOwnProperty)
// ❌ 旧方式的问题
const obj = Object.create(null); // 没有原型,没有 hasOwnProperty 方法
// obj.hasOwnProperty('key') → TypeError
// ✅ Object.hasOwn 安全
console.log(Object.hasOwn(obj, 'key')); // false
console.log(Object.hasOwn({ a: 1 }, 'a')); // true
console.log(Object.hasOwn({ a: 1 }, 'toString')); // false(继承属性)
3. Array.prototype.at()
const arr = [1, 2, 3, 4, 5];
// 负索引访问
arr.at(-1); // 5(最后一个)
arr.at(-2); // 4
arr.at(0); // 1
// 旧方式
arr[arr.length - 1]; // 5(繁琐)
// 字符串也支持
'hello'.at(-1); // 'o'
4. 类字段与私有方法(正式化)
class Counter {
// 公有字段
count = 0;
static instances = 0;
// 私有字段(真正私有,不可外部访问)
#step;
#history = [];
constructor(step = 1) {
this.#step = step;
Counter.instances++;
}
// 私有方法
#record(value) {
this.#history.push(value);
}
increment() {
this.count += this.#step;
this.#record(this.count);
return this;
}
get history() { return [...this.#history]; }
// 静态私有字段
static #MAX = 1000;
static getMax() { return Counter.#MAX; }
// 私有字段存在检查(in 操作符)
static isCounter(obj) {
return #step in obj; // 检查是否有私有字段 #step
}
}
const c = new Counter(5);
c.increment().increment();
console.log(c.count); // 10
// c.#step; // SyntaxError:私有字段不可外部访问
// '#step' in c; // false(#step 是私有语法,不是字符串)
Counter.isCounter(c); // true
5. Top-level await(正式化)
// 在 ES Module 顶层直接使用 await(无需包装在 async 函数中)
// module.js
const data = await fetch('/api/config').then(r => r.json());
export const config = data;
// 模块加载会等待 await 完成后才将模块标记为"已加载"
// 依赖此模块的其他模块也会等待
ES2023 核心特性
// 1. Array.prototype.toSorted / toReversed / toSpliced(不可变版本)
const arr = [3, 1, 4, 1, 5];
arr.toSorted(); // [1, 1, 3, 4, 5](新数组,原数组不变)
arr.toReversed(); // [5, 1, 4, 1, 3](新数组)
arr.toSpliced(2, 1, 99); // [3, 1, 99, 1, 5](新数组)
arr.with(0, 99); // [99, 1, 4, 1, 5](新数组,替换索引 0)
console.log(arr); // [3, 1, 4, 1, 5](原数组未变)
// 2. Array.prototype.findLast / findLastIndex
const nums = [1, 2, 3, 4, 5];
nums.findLast(n => n % 2 === 0); // 4(从后往前找)
nums.findLastIndex(n => n % 2 === 0); // 3
// 3. Symbol.dispose(TC39 Explicit Resource Management)
using resource = getResource(); // 块结束时自动调用 resource[Symbol.dispose]()
ES2024 核心特性
1. Promise.withResolvers
// ❌ 旧写法:resolve/reject 泄露到外部
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// ✅ Promise.withResolvers(官方支持)
const { promise, resolve, reject } = Promise.withResolvers();
// 典型用场:事件转 Promise
function waitForEvent(emitter, event) {
const { promise, resolve, reject } = Promise.withResolvers();
emitter.once(event, resolve);
emitter.once('error', reject);
return promise;
}
// 用途:Deferred 模式
class Deferred {
constructor() {
Object.assign(this, Promise.withResolvers());
}
}
2. Object.groupBy / Map.groupBy
const items = [
{ name: 'Apple', type: 'fruit', price: 1.5 },
{ name: 'Banana', type: 'fruit', price: 0.5 },
{ name: 'Carrot', type: 'veggie', price: 0.8 },
{ name: 'Daikon', type: 'veggie', price: 1.2 },
];
// Object.groupBy → 普通对象(key 必须是 string/symbol)
const byType = Object.groupBy(items, item => item.type);
console.log(byType);
// {
// fruit: [{ name: 'Apple', ... }, { name: 'Banana', ... }],
// veggie: [{ name: 'Carrot', ... }, { name: 'Daikon', ... }]
// }
// Map.groupBy → Map(key 可以是任意类型)
const byPriceRange = Map.groupBy(items, item =>
item.price < 1 ? 'cheap' : 'expensive'
);
3. ArrayBuffer.prototype.transfer
// 零拷贝所有权转移(前文 TypedArray 章节已详细介绍)
const buf = new ArrayBuffer(1024);
const newBuf = buf.transfer(); // buf 变成 detached,newBuf 持有内存
Temporal API(未来)
Temporal 是替代 Date 的全新日期时间 API(TC39 Stage 3)。
Date 的痛点
// ❌ Date 的问题:
// 1. 月份从 0 开始(反直觉)
new Date(2024, 0, 15); // 1月15日,不是0月!
// 2. 可变性(Mutable)
const d = new Date();
d.setFullYear(2025); // 原地修改,易出 bug
// 3. 时区处理混乱
new Date('2024-01-15').getDate(); // 在不同时区可能返回 14!
// 4. 字符串解析行为不一致(跨浏览器)
new Date('2024-1-5'); // Invalid Date(某些浏览器)
Temporal 的设计理念
// ✅ Temporal 特性(需 polyfill 或等待原生支持)
import Temporal from '@js-temporal/polyfill';
// 1. 不可变(所有操作返回新对象)
const today = Temporal.PlainDate.today();
const tomorrow = today.add({ days: 1 }); // today 不变
// 2. 月份从 1 开始
const date = new Temporal.PlainDate(2024, 1, 15); // 1月15日
// 3. 明确的类型体系
Temporal.PlainDate // 纯日期(无时间,无时区)
Temporal.PlainTime // 纯时间
Temporal.PlainDateTime // 日期+时间(无时区)
Temporal.ZonedDateTime // 日期+时间+时区(最完整)
Temporal.Instant // UTC 时间点(类似 Date)
Temporal.Duration // 时间段
// 4. 时区处理明确
const zdt = Temporal.ZonedDateTime.from('2024-01-15T10:00:00+08:00[Asia/Shanghai]');
zdt.timeZoneId; // 'Asia/Shanghai'
zdt.withTimeZone('America/New_York'); // 转换时区,日历时间不同,绝对时间相同
// 5. 精确的比较和算术
const d1 = Temporal.PlainDate.from('2024-01-01');
const d2 = Temporal.PlainDate.from('2024-03-15');
const diff = d1.until(d2); // Temporal.Duration
diff.days; // 74
diff.months; // 取决于 largestUnit 设置
手写验证:Polyfill 实现
Promise.withResolvers Polyfill
if (!Promise.withResolvers) {
Promise.withResolvers = function() {
let resolve, reject;
const promise = new this((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
}
// 测试
const { promise, resolve } = Promise.withResolvers();
setTimeout(() => resolve(42), 100);
console.log(await promise); // 42
Object.groupBy Polyfill
if (!Object.groupBy) {
Object.groupBy = function(iterable, keyFn) {
const result = Object.create(null);
let index = 0;
for (const item of iterable) {
const key = String(keyFn(item, index++));
if (!(key in result)) {
result[key] = [];
}
result[key].push(item);
}
return result;
};
}
if (!Map.groupBy) {
Map.groupBy = function(iterable, keyFn) {
const result = new Map();
let index = 0;
for (const item of iterable) {
const key = keyFn(item, index++);
if (!result.has(key)) {
result.set(key, []);
}
result.get(key).push(item);
}
return result;
};
}
WeakRef 缓存实现
class WeakRefCache {
#cache = new Map();
#registry;
constructor() {
this.#registry = new FinalizationRegistry((key) => {
// GC 回收对象后,自动清理 Map 中的 WeakRef
const ref = this.#cache.get(key);
if (ref && ref.deref() === undefined) {
this.#cache.delete(key);
console.log(`缓存项 "${key}" 已因 GC 失效`);
}
});
}
set(key, value) {
const ref = new WeakRef(value);
this.#cache.set(key, ref);
this.#registry.register(value, key, ref); // 第三参数是 unregisterToken
return this;
}
get(key) {
const ref = this.#cache.get(key);
if (!ref) return undefined;
const value = ref.deref();
if (value === undefined) {
this.#cache.delete(key);
}
return value;
}
has(key) {
return this.get(key) !== undefined;
}
delete(key) {
this.#cache.delete(key);
}
get size() {
return this.#cache.size; // 注意:可能包含已被 GC 的条目
}
}
// 测试
const cache = new WeakRefCache();
let obj = { heavy: new ArrayBuffer(1024 * 1024 * 50) }; // 50MB
cache.set('big-data', obj);
console.log(cache.has('big-data')); // true
obj = null; // 允许 GC 回收
// 强制 GC(仅测试环境):global.gc?.();
// 某时刻后:cache.has('big-data') → false
深度追问
Q1:?. 和 ?? 各自解决了什么痛点,能否混用?
?.(Optional Chaining):防止在 null/undefined 上访问属性导致 TypeError。
??(Nullish Coalescing):区分 null/undefined 与其他 falsy 值,提供精准默认值。
两者经常组合使用:config?.timeout ?? 5000——先安全访问,再提供默认值。
|| 的问题:0 || 5000 返回 5000,而用户确实可能设置 timeout 为 0;?? 则正确返回 0。
Q2:WeakRef 与 WeakMap 的应用场景区别?
WeakMap:键是弱引用,值是强引用,键被 GC 时整个键值对消失。用于给对象附加元数据(不影响对象生命周期)。
WeakRef:直接对目标对象弱引用,deref() 可能返回 undefined。用于缓存场景——需要保存引用但不阻止回收,内存压力大时自动失效。
Q3:Error.cause 如何改善错误处理体验?
传统做法"catch and rethrow"会丢失原始错误栈,或需要在 message 中手动拼接。Error.cause 提供标准的错误链,调试工具可以完整展示每一层错误的原始信息,日志系统也可递归遍历 cause 链收集完整上下文。
Q4:Temporal API 为何要区分 PlainDate、PlainDateTime、ZonedDateTime?
这是类型精确性的体现:
PlainDate:只有日期,不涉及时间,不可能有时区歧义(生日、节日)PlainDateTime:有日期和时间,但不关联时区(闹钟时间、会议室预订)ZonedDateTime:绑定具体时区,表示全球唯一的时间点(日志时间戳、预约通知)
Date 混淆了这些概念,导致无数时区 bug。Temporal 强制开发者明确语义。