ES2020-ES2024核心新特性全解—从Optional Chaining到Temporal API|新宇宙博客返回列表ES2020-ES2024 核心特性
详解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 核心特性
- ES2021 核心特性
- ES2022 核心特性
- ES2023 核心特性
- ES2024 核心特性
- Temporal API(未来)
- 手写验证:Polyfill 实现
- 深度追问
ES2020 核心特性
1. Optional Chaining ?.
解决痛点:深层属性访问时的空值判断链
const city = user && user.address && user.address.city;
city = user?.?.;
user?.?.();
arr?.[]?.();
result = maybeFunction?.();
.(?.?.?.);
.(?.);
.(?.());
const
address
city
getName
0
toUpperCase
const
console
log
null
a
b
c
console
log
''
length
console
log
0
toString
2. Nullish Coalescing ??
解决痛点:区分 null/undefined 与其他 falsy 值(0, '', false)
const port = userConfig.port || 3000;
const port = userConfig.port ?? 3000;
const name = user.name ?? 'Anonymous';
const count = data.count ?? 0;
const theme = config?.theme?.color ?? '#333';
let x = null;
x ??= 'default';
console.log(x);
let y = 0;
y ??= 'default';
console.log(y);
3. Promise.allSettled
const promises = [
fetch('/api/users'),
fetch('/api/invalid-endpoint'),
fetch('/api/products'),
];
const results = await Promise.allSettled(promises);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${i} 成功:`, result.value);
} else {
console.warn(`请求 ${i} 失败:`, result.reason.message);
}
});
4. BigInt
const big = 9007199254740993n;
const max = BigInt(Number.MAX_SAFE_INTEGER);
console.log(max + 1n);
console.log(max + 1);
big + 1n;
Number(42n);
String(42n);
42n.toString(16);
5. globalThis
console.log(globalThis);
const global = typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global
: typeof self !== 'undefined' ? self
: this;
globalThis.myLibrary = { version: '1.0' };
ES2021 核心特性
1. WeakRef
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) {
this.#store.delete(key);
return undefined;
}
return value;
}
}
const domCache = new Map();
function cacheElement(id, el) {
domCache.set(id, new WeakRef(el));
}
function getElement(id) {
return domCache.get(id)?.deref();
}
const ref = new WeakRef({ data: 'important' });
const obj = ref.deref();
if (obj) {
processData(obj.data);
}
2. FinalizationRegistry
const registry = new FinalizationRegistry((heldValue) => {
console.log(`对象 "${heldValue}" 已被回收`);
});
let bigObject = { data: new ArrayBuffer(1024 * 1024 * 100) };
registry.register(bigObject, 'big-object-100mb');
bigObject = null;
3. 逻辑赋值运算符
let a = 1;
a &&= 2;
let b = 0;
b &&= 2;
let c = null;
c ||= 'default';
let d = 'existing';
d ||= 'default';
obj.cache ??= {};
obj.cache.key ||= computeExpensiveValue();
4. String.prototype.replaceAll
'a.b.c'.replace(/\./g, '-');
'a.b.c'.replaceAll('.', '-');
'hello world'.replaceAll('o', (match, offset) => `[${offset}]`);
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) {
throw new Error(`获取用户 ${id} 失败`, { cause: e });
}
}
try {
await fetchUser(42);
} catch (e) {
console.error(e.message);
console.error(e.cause?.message);
console.error(e.cause?.cause);
}
2. Object.hasOwn(替代 hasOwnProperty)
const obj = Object.create(null);
console.log(Object.hasOwn(obj, 'key'));
console.log(Object.hasOwn({ a: 1 }, 'a'));
console.log(Object.hasOwn({ a: 1 }, 'toString'));
3. Array.prototype.at()
const arr = [1, 2, 3, 4, 5];
arr.at(-1);
arr.at(-2);
arr.at(0);
arr[arr.length - 1];
'hello'.at(-1);
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; }
static isCounter(obj) {
return #step in obj;
}
}
const c = new Counter(5);
c.increment().increment();
console.log(c.count);
Counter.isCounter(c);
5. Top-level await(正式化)
const data = await fetch('/api/config').then(r => r.json());
export const config = data;
ES2023 核心特性
const arr = [3, 1, 4, 1, 5];
arr.toSorted();
arr.toReversed();
arr.toSpliced(2, 1, 99);
arr.with(0, 99);
console.log(arr);
const nums = [1, 2, 3, 4, 5];
nums.findLast(n => n % 2 === 0);
nums.findLastIndex(n => n % 2 === 0);
using resource = getResource();
ES2024 核心特性
1. Promise.withResolvers
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const { promise, resolve, reject } = Promise.withResolvers();
function waitForEvent(emitter, event) {
const { promise, resolve, reject } = Promise.withResolvers();
emitter.once(event, resolve);
emitter.once('error', reject);
return promise;
}
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 },
];
const byType = Object.groupBy(items, item => item.type);
console.log(byType);
const byPriceRange = Map.groupBy(items, item =>
item.price < 1 ? 'cheap' : 'expensive'
);
3. ArrayBuffer.prototype.transfer
const buf = new ArrayBuffer(1024);
const newBuf = buf.transfer();
Temporal API(未来)
Temporal 是替代 Date 的全新日期时间 API(TC39 Stage 3)。
Date 的痛点
new Date(2024, 0, 15);
const d = new Date();
d.setFullYear(2025);
new Date('2024-01-15').getDate();
new Date('2024-1-5');
Temporal 的设计理念
import Temporal from '@js-temporal/polyfill';
const today = Temporal.PlainDate.today();
const tomorrow = today.add({ days: 1 });
const date = new Temporal.PlainDate(2024, 1, 15);
Temporal.PlainDate
Temporal.PlainTime
Temporal.PlainDateTime
Temporal.ZonedDateTime
Temporal.Instant
Temporal.Duration
const zdt = Temporal.ZonedDateTime.from('2024-01-15T10:00:00+08:00[Asia/Shanghai]');
zdt.timeZoneId;
zdt.withTimeZone('America/New_York');
const d1 = Temporal.PlainDate.from('2024-01-01');
const d2 = Temporal.PlainDate.from('2024-03-15');
const diff = d1.until(d2);
diff.days;
diff.months;
手写验证: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);
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) => {
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);
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;
}
}
const cache = new WeakRefCache();
let obj = { heavy: new ArrayBuffer(1024 * 1024 * 50) };
cache.set('big-data', obj);
console.log(cache.has('big-data'));
obj = null;
深度追问
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 强制开发者明确语义。