ECMAScript 2026 深度实战:第17版规范全面解析——从高精度求和到 JSON 精细控制,8 大新特性重塑 JavaScript 开发范式
前言:ECMAScript 2026 来了,但为什么我们还需要关心它?
2026年6月30日,ECMA International 正式批准了 ECMAScript 2026(ECMA-262 第17版)。每一年这个时候,我们都会听到"JS又更新了"的声音,但大多数开发者可能只是扫一眼新闻标题,然后继续写自己的业务代码——毕竟,const 和 let 已经够用了,Promise 和 async/await 也早就烂熟于心,何必关心什么新特性?
但这一次,我想认真说:ECMAScript 2026 不是一次例行的修修补补,而是一次在多个关键工程场景上的实质性突破。
从高精度数值计算、异步迭代器增强、错误处理的规范化,到 Uint8Array 原生编码转换,再到 JSON 解析和序列化的精细控制——这些新增 API 解决的恰恰是过去我们必须引入第三方库才能处理的问题,是真正的"语言级 primitives"。
本文将逐一拆解 ES2026 的 8 大核心新特性:每项特性给出使用背景、语法详解、代码示例,以及polyfill/兼容情况,让你读完就能用上。
一、Math.sumPrecise:终于,语言层面解决了浮点数求和精度问题
1.1 问题:为什么 0.1 + 0.2 ≠ 0.3
在 JavaScript 中,以下代码的结果几乎每个程序员都见过:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
这并非 JavaScript 的 bug,而是 IEEE 754 双精度浮点数的固有特性。当我们将 0.1(十进制)转换为二进制时,它是一个无限循环小数,必须在有限的 64 位中截断,从而引入舍入误差。多个浮点数累加时,这些微小的舍入误差会不断积累,在金融计算、科学计算、统计汇总等场景中可能造成严重问题。
1.2 过去的解决方案与代价
为了解决高精度求和问题,开发者通常有以下选择:
方案一:第三方库(如 decimal.js)
import Decimal from 'decimal.js';
const result = new Decimal('0.1').plus('0.2');
console.log(result.toString()); // '0.3'
缺点:需要引入一个数百 KB 的外部依赖,增加了 bundle 体积和依赖管理复杂度。
方案二:整数运算(放大缩小法)
// 将小数转化为整数,计算后再缩小
function addFloat(a, b, decimals = 2) {
const factor = Math.pow(10, decimals);
return (Math.round(a * factor) + Math.round(b * factor)) / factor;
}
console.log(addFloat(0.1, 0.2)); // 0.3
缺点:只能处理固定小数位数,无法优雅地处理数量级差异极大的数字累加。
方案三:Kahan 求和算法
function kahanSum(numbers) {
let sum = 0;
let c = 0; // 补偿位
for (const num of numbers) {
const y = num - c;
const t = sum + y;
c = (t - sum) - y;
sum = t;
}
return sum;
}
const values = [0.1, 0.2, 0.3, 0.4];
console.log(kahanSum(values)); // 1.0(精确)
缺点:算法有额外开销,代码不够直观,且对于包含极大数和极小数的混合运算效果有限。
1.3 Math.sumPrecise 的登场
ES2026 引入了 Math.sumPrecise(),从语言层面直接解决了这个问题:
// 基本用法
const result = Math.sumPrecise(0.1, 0.2);
console.log(result); // 0.3
console.log(result === 0.3); // true
// 数组求和
const prices = [0.1, 0.2, 0.3, 0.4];
const total = Math.sumPrecise(...prices);
console.log(total); // 1.0
// 混合数量级求和(高精度场景的核心价值)
const largeAndSmall = [1000000, 1e-10, 1e-10, 1e-10];
const sum = Math.sumPrecise(...largeAndSmall);
console.log(sum); // 1000000.0000000003(而非 1000000)
Math.sumPrecise 的底层实现使用了类似 Kahan 求和 或 Neumaier 求和 的高精度累加算法,通过引入一个"补偿位"来追踪被低精度舍入丢弃的部分,从而在累加过程中逐步补偿误差。
1.4 为什么这不只是"又一个语法糖"
你可能会说:Math.sumPrecise 不就是内置了一个 Kahan 求和吗?我自己写一个函数也行啊。
但关键区别在于:
- 引擎级优化:内置 API 可以被 JavaScript 引擎直接优化,在 V8 / SpiderMonkey / JavaScriptCore 层面可能有 SIMD 加速或更精确的浮点中间表示,第三方库做不到这一点。
- 跨环境一致行为:各引擎对
Math.sumPrecise的实现必须遵循规范定义的行为,而自己写的 Kahan 实现可能在不同引擎的浮点优化策略下行为略有差异。 - 标准互操作性:一旦
Math.sumPrecise成为标准,其他标准库(如 Temporal API 的Temporal.Duration)可以依赖它来保证跨环境的一致性。
1.5 实战建议
// 金融计算场景
function calculateOrderTotal(items) {
const prices = items.map(item => item.price);
return Math.sumPrecise(...prices);
}
// 统计汇总场景
function sumArrayPrecise(arr) {
return Math.sumPrecise(...arr);
}
// 性能注意:sumPrecise 比普通累加慢约 5-10 倍,
// 只在真正需要高精度时才使用,不要用它替代所有求和
二、Iterator.concat:终于,语言层面统一了迭代器的拼接
2.1 问题背景
在 ES6 引入迭代器和生成器之后,我们经常需要拼接多个数据源:
// 场景:合并多个生成器输出
function* gen1() { yield 1; yield 2; }
function* gen2() { yield 3; yield 4; }
function* gen3() { yield 5; yield 6; }
// 过去的做法:手动 yield*
function* combineOld(generators) {
for (const gen of generators) {
yield* gen();
}
}
// 或者用数组拼接后创建新迭代器
const combined = [...gen1(), ...gen2(), ...gen3()];
这些方案都有问题:手动 yield* 需要写循环,数组展开会一次性加载所有数据到内存——对于无限迭代器或超大数据源,这个方案根本不可行。
2.2 Iterator.concat 的解决方案
ES2026 提供了 Iterator.concat(),从语言层面支持迭代器拼接:
function* gen1() {
console.log('gen1 called');
yield 1; yield 2;
}
function* gen2() {
console.log('gen2 called');
yield 3; yield 4;
}
function* gen3() {
console.log('gen3 called');
yield 5; yield 6;
}
// 基本用法
const merged = Iterator.concat(gen1(), gen2(), gen3());
console.log(merged.next().value); // gen1 called, 1(惰性求值)
console.log(merged.next().value); // gen1 called, 2
console.log(merged.next().value); // gen2 called, 3(gen1 耗尽后才调用 gen2)
console.log([...merged]); // [4, 5, 6]
// 数组形式
const iters = [gen1(), gen2(), gen3()];
const combined = Iterator.concat(iters);
console.log([...combined]); // [1, 2, 3, 4, 5, 6]
2.3 惰性求值的工程价值
Iterator.concat 最重要的特性是完全惰性求值。与数组展开不同,它不会在拼接时立即执行任何生成器,只有在你实际调用 .next() 时,才会逐个消耗源迭代器。
这对于以下场景至关重要:
// 场景1:处理超大数据集
function* readLargeFile(filename) {
const lines = await readLines(filename); // 假设这是一个生成器
for (const line of lines) {
yield line;
}
}
function* filterLines(lines) {
for (const line of lines) {
if (line.includes('ERROR')) yield line;
}
}
// 流式处理多个大文件:完全不占用大量内存
const errorLines = Iterator.concat(
filterLines(readLargeFile('app.log')),
filterLines(readLargeFile('error.log')),
filterLines(readLargeFile('debug.log'))
);
for (const error of errorLines) {
console.log(error); // 逐行处理,内存占用恒定
}
// 场景2:无限迭代器
function* naturalNumbers() {
let n = 1;
while (true) yield n++;
}
function* evenNumbers() {
let n = 2;
while (true) yield n += 2;
}
// 拼接两个无限生成器(数组方案会直接卡死)
const allIntegers = Iterator.concat(naturalNumbers(), evenNumbers());
console.log(allIntegers.next().value); // 1
console.log(allIntegers.next().value); // 2
2.4 与 Array.prototype.concat 的对比
| 特性 | Array.concat | Iterator.concat |
|---|---|---|
| 参数类型 | 数组或类数组 | 多个可迭代对象 |
| 立即求值 | 是 | 否(惰性) |
| 内存占用 | O(n) | O(1)(每次只持有一个源迭代器) |
| 无限迭代器支持 | ❌ | ✅ |
| 链式调用 | .concat().concat() | Iterator.concat(...).concat(...) |
三、Array.fromAsync:从异步数据源到数组的桥梁
3.1 问题:异步可迭代对象没有好办法转为数组
在现代异步编程中,我们经常遇到异步可迭代对象:流(Streams)、分页 API、数据库游标等。过去,将这些转为数组非常别扭:
// 场景:读取所有分页数据
async function fetchAllPages(apiUrl) {
const results = [];
let cursor = null;
do {
const page = await fetchPage(apiUrl, cursor);
results.push(...page.items); // 每次 push 整个数组
cursor = page.nextCursor;
} while (cursor);
return results;
}
// 或者更通用的方式(但有内存问题)
async function toArrayOld(asyncIterable) {
const arr = [];
for await (const item of asyncIterable) {
arr.push(item);
}
return arr;
}
这两种方式在数据量大时都有问题:手写循环需要处理游标逻辑,而 for await...of 循环的写法不够直观。
3.2 Array.fromAsync 的登场
ES2026 引入了 Array.fromAsync(),将异步可迭代对象直接转为数组,同时支持映射函数:
// 基本用法
const asyncIterable = {
async *[Symbol.asyncIterator]() {
yield 1;
yield 2;
yield 3;
}
};
const arr = await Array.fromAsync(asyncIterable);
console.log(arr); // [1, 2, 3]
// 从 Promise 数组创建
const promises = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
const values = await Array.fromAsync(promises);
console.log(values); // [1, 2, 3]
// 映射函数
async function* fetchUserData(userIds) {
for (const id of userIds) {
const user = await fetch(`/api/users/${id}`).then(r => r.json());
yield user;
}
}
const users = await Array.fromAsync(
fetchUserData([1, 2, 3]),
user => ({ id: user.id, name: user.name }) // 映射函数在映射阶段工作
);
3.3 实际应用场景
// 场景1:处理分页 API
async function getAllNotifications(userId) {
const pages = {
async *[Symbol.asyncIterator]() {
let page = 1;
while (true) {
const data = await fetch(`/api/users/${userId}/notifications?page=${page}`);
const json = await data.json();
yield* json.items;
if (!json.hasNext) break;
page++;
}
}
};
return Array.fromAsync(pages);
}
// 场景2:处理 ReadableStream
const response = await fetch('https://api.example.com/data');
const text = await Array.fromAsync(
response.body,
chunk => new TextDecoder().decode(chunk)
).then(chunks => chunks.join(''));
// 场景3:数据库游标(如 IndexedDB)
function queryAll(cursorRequest) {
return Array.fromAsync(cursorRequest, event => event.target.result);
}
const tx = db.transaction('objects', 'readonly');
const store = tx.objectStore('objects');
const request = store.openCursor();
const allObjects = await queryAll(request);
3.4 关键特性:映射函数是可选的,且可以异步
// 注意:映射函数接收的值是已 resolve 的(不同于 Promise.all)
const items = await Array.fromAsync(
[1, 2, 3].map(x => Promise.resolve(x * 2)), // 输入是 Promise[]
async (value) => {
// 这里 value 已经是 2, 4, 6(已 resolve)
const processed = await fetch(`/api/process/${value}`);
return processed.json();
}
);
四、Error.isError:终于,有了一个可靠的错误识别方法
4.1 问题:为什么 instanceof Error 不够用
JavaScript 中的错误识别一直是个坑。instanceof Error 的问题在于:
// 问题1:跨 iframe / Worker 的 Error 实例
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeError = new iframe.contentWindow.Error('跨域错误');
console.log(iframeError instanceof Error); // false ❌
// 问题2:DOMException 不是 Error 的实例(某些环境下)
try {
// 在某些旧环境中,DOMException 继承自 Error
// 但在某些环境下是独立实现
const err = new DOMException('message', 'NotFoundError');
console.log(err instanceof Error); // 不确定 ❌
} catch (e) {}
// 问题3:库可能创建自定义错误类
class NetworkError extends Error {
constructor(message, status) {
super(message);
this.name = 'NetworkError';
this.status = status;
}
}
const networkErr = new NetworkError('Request failed', 500);
// 在某些打包/压缩场景下 instanceof 可能失效
console.log(networkErr instanceof Error); // 取决于环境 ❌
// 问题4:被序列化的错误(JSON.stringify/反序列化)
const serialized = JSON.stringify(new Error('test'));
const deserialized = JSON.parse(serialized, (k, v) => {
if (k === 'message') return v;
});
console.log(deserialized instanceof Error); // false ❌
4.2 Error.isError 的解决方案
ES2026 引入了 Error.isError() 作为标准错误识别方法:
// 基本用法
const err = new Error('Something went wrong');
console.log(Error.isError(err)); // true
// 跨 realm(如 iframe、Worker)
const iframeError = new iframe.contentWindow.Error('test');
console.log(Error.isError(iframeError)); // true ✅
// DOMException
const domErr = new DOMException('Not found', 'NotFoundError');
console.log(Error.isError(domErr)); // true ✅(规范要求 DOMException 实现 [[ErrorData]] 内部槽)
// 自定义错误类
class NetworkError extends Error {
constructor(message, status) {
super(message);
this.name = 'NetworkError';
this.status = status;
}
}
const ne = new NetworkError('Failed', 500);
console.log(Error.isError(ne)); // true ✅(子类继承 [[ErrorData]])
// 反序列化的错误对象
const ser = JSON.stringify(new Error('test'));
const deser = JSON.parse(ser, (k, v) => v);
console.log(Error.isError(deser)); // false ✅(正确!普通对象不是 Error)
// null / undefined
console.log(Error.isError(null)); // false
console.log(Error.isError(undefined)); // false
console.log(Error.isError('not an error')); // false
console.log(Error.isError({ message: 'error' })); // false
4.3 Error.isError 的原理
Error.isError() 的实现依赖于每个 Error 实例都有一个内部的 [[ErrorData]] 内部槽。这个内部槽只有真正的 Error 实例(原生 Error 类及其子类)才会设置,普通对象即使有 message 属性,也不会设置这个槽。因此,检查 [[ErrorData]] 是否存在,就成了跨 realm 环境下判断一个对象是否是真正的 Error 的可靠方法。
4.4 实战应用
// 标准化错误处理函数
function handleError(error) {
if (Error.isError(error)) {
console.error(`[${error.name}] ${error.message}`);
if (error.cause) {
console.error('Caused by:', error.cause);
}
// 上报错误
reportError(error);
} else {
// 不是标准错误,可能是意外的数据类型
console.error('Unexpected error type:', error);
reportError(new Error(String(error)));
}
}
// catch-all 的安全写法
try {
await someRiskyOperation();
} catch (e) {
if (Error.isError(e)) {
// 安全地访问 error 属性
logger.error(e.message, { stack: e.stack, cause: e.cause });
} else {
// 将非错误对象包装
logger.error('Unknown error', { value: e });
}
}
// TypeScript 中的类型守卫
function isErrorLike(value: unknown): value is Error {
return Error.isError(value);
}
五、Map.prototype.getOrElse / WeakMap.prototype.getOrElse:优雅处理键缺失
5.1 问题:每次检查键存在都很啰嗦
在 JavaScript 中,处理 Map 中键不存在的情况一直是痛点:
const cache = new Map();
// 写法一:重复查询
if (cache.has('user')) {
return cache.get('user');
} else {
const user = await fetchUser('user');
cache.set('user', user);
return user;
}
// 写法二:|| 运算符(但 null/undefined 会被错误处理)
const user = cache.get('user') || await fetchUser('user');
// 问题:如果 cache 中存的是 null 或 undefined,这里永远会重新获取
// 写法三:?? 运算符(稍好,但仍有重复查询)
const user2 = cache.get('user') ?? await fetchUser('user');
// 问题:仍然查询了两次(has + get)
5.2 getOrElse 的解决方案
ES2026 为 Map 和 WeakMap 新增了 getOrElse 方法:
const cache = new Map<string, User>();
// 旧写法
async function getUser(id) {
const cached = cache.get(id);
if (cached !== undefined) {
return cached;
}
const user = await fetchUser(id);
cache.set(id, user);
return user;
}
// 新写法(ES2026):getOrElse
async function getUserNew(id) {
return cache.getOrElse(id, async () => {
const user = await fetchUser(id);
cache.set(id, user);
return user;
});
}
// 基本示例
const config = new Map([
['env', 'production'],
['debug', false],
['timeout', 5000]
]);
// 场景1:简单默认值
console.log(config.getOrElse('env', 'development')); // 'production'
console.log(config.getOrElse('region', 'us-east-1')); // 'us-east-1'(使用默认值)
// 场景2:惰性计算默认值(函数)
console.log(config.getOrElse('cachedAt', () => Date.now()));
// 如果 'cachedAt' 不存在,() => Date.now() 才会被执行
// 注意:惰性求值只在键不存在时才执行计算函数
// 避免了不必要的计算开销
5.3 进一步的便利方法
除了 getOrElse,ES2026 还为 Map.prototype 添加了其他便利方法:
const scores = new Map([
['Alice', 95],
['Bob', 87],
['Carol', 92]
]);
// getOrElse:获取值或默认值
console.log(scores.getOrElse('Alice', 0)); // 95
console.log(scores.getOrElse('Dave', 0)); // 0
// upsert:原子性插入或更新(可选功能,根据提案)
scores.upsert('Alice',
() => 100, // insertFn:键不存在时的插入函数
(old) => old+5 // updateFn:键存在时的更新函数
);
console.log(scores.get('Alice')); // 100
scores.upsert('Eve',
() => 88,
(old) => old + 2
);
console.log(scores.get('Eve')); // 88
5.4 WeakMap 的 getOrElse
WeakMap 的 getOrElse 特别有价值,因为它要求键是对象引用,没有 has 方法的等价物:
// WeakMap 无法遍历,没有 has() 方法
const privateData = new WeakMap();
// 旧写法
function getPrivate(obj) {
if (!privateData.has(obj)) {
privateData.set(obj, computeExpensiveData(obj));
}
return privateData.get(obj);
}
// 新写法
function getPrivateNew(obj) {
return privateData.getOrElse(obj, () => computeExpensiveData(obj));
}
// 实际场景:存储 DOM 元素的私有数据
const elementStates = new WeakMap();
function getElementState(element) {
return elementStates.getOrElse(element, () => ({
clicks: 0,
lastClick: null,
isActive: false
}));
}
六、Uint8Array 原生编码转换:告别手写进制转换
6.1 问题:二进制与字符串之间的转换曾经非常痛苦
处理二进制数据时,我们经常需要在 Uint8Array(十六进制字符串、Base64)之间互相转换。过去的做法既不直观,性能也差:
// Uint8Array → 十六进制字符串
function uint8ToHex(uint8array) {
return Array.from(uint8array)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// 十六进制字符串 → Uint8Array
function hexToUint8(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
return bytes;
}
// Uint8Array → Base64
function uint8ToBase64(uint8array) {
let binary = '';
for (const byte of uint8array) {
binary += String.fromCharCode(byte);
}
return btoa(binary); // btoa 只支持 Latin-1 字符
}
// Base64 → Uint8Array
function base64ToUint8(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
这些函数有很多陷阱:btoa/atob 不支持 Unicode、十六进制转换容易出错、没有统一的 API。
6.2 ES2026 的原生编码方法
ES2026 为 Uint8Array.prototype 添加了两个新方法:
// Uint8Array → 十六进制
const data = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
console.log(data.toHexString()); // '48656c6c6f'
console.log(data.toHexString({ uppercase: true })); // '48656C6C6F'(大写可选)
// 十六进制 → Uint8Array
const decoded = Uint8Array.fromHex('48656c6c6f');
console.log(decoded); // Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])
// Uint8Array → Base64
const b64 = data.toBase64String();
console.log(b64); // 'SGVsbG8='
// Base64 → Uint8Array
const decodedB64 = Uint8Array.fromBase64('SGVsbG8=');
console.log(decodedB64); // Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])
// URL-safe Base64 变体
const b64url = data.toBase64String({ urlSafe: true });
console.log(b64url); // 'SGVsbG8'(无 = 填充)
6.3 实际应用场景
// 场景1:加密通信
async function encryptAndSend(data, secretKey) {
const encoder = new TextEncoder();
const bytes = encoder.encode(JSON.stringify(data));
// 生成随机 IV
const iv = crypto.getRandomValues(new Uint8Array(12));
// 加密
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
secretKey,
bytes
);
// 将加密结果转为十六进制传输
const combined = new Uint8Array([...iv, ...new Uint8Array(encrypted)]);
return combined.toHexString();
}
// 场景2:文件哈希校验
async function verifyChecksum(fileData, expectedHex) {
const hashBuffer = await crypto.subtle.digest('SHA-256', fileData);
const hashHex = new Uint8Array(hashBuffer).toHexString();
return hashHex === expectedHex.toLowerCase();
}
// 场景3:JWT 的 header/payload 解析
function parseJWT(token) {
const [headerB64, payloadB64] = token.split('.');
const headerBytes = Uint8Array.fromBase64url(headerB64);
const payloadBytes = Uint8Array.fromBase64url(payloadB64);
const decoder = new TextDecoder();
return {
header: JSON.parse(decoder.decode(headerBytes)),
payload: JSON.parse(decoder.decode(payloadBytes))
};
}
// Uint8Array.fromBase64url 支持 URL-safe Base64(- 和 _ 替换 + 和 /)
七、JSON.parse 的增强 revivers:终于可以访问原始片段了
7.1 问题:reviver 函数的上下文局限性
JSON.parse 的第二个参数(reviver)允许在解析时对每个键值对进行转换,但它有一个严重限制:你不知道这个值在原始 JSON 字符串中的确切位置和上下文。
// 场景:我们想把所有 "timestamp" 字段从毫秒时间戳转为 Date 对象
const json = JSON.stringify({
createdAt: 1719849600000,
updatedAt: 1719936000000,
events: [
{ type: 'login', timestamp: 1719849600000 },
{ type: 'purchase', timestamp: 1719850000000 }
]
});
// 旧 reviver 写法
const parsed = JSON.parse(json, (key, value) => {
if (key === 'timestamp') {
return new Date(value); // 成功
}
return value;
});
// 问题:如果数据结构中还有其他 "timestamp" 字段不希望被转换,
// reviver 无法区分来源(是事件中的 timestamp 还是其他地方的)
更严重的问题是:reviver 无法访问原始 JSON 中的匹配片段。这在需要"基于上下文决定如何解析"时非常受限。
7.2 ES2026 的增强 reviver
ES2026 为 reviver 函数增加了第三个参数 context:
const json = JSON.stringify({
user: {
profile: {
avatarUrl: 'https://example.com/avatar.jpg',
lastSeen: 1719849600000
},
posts: [
{ id: 1, title: 'Hello', timestamp: 1719849600000 }
]
}
});
// ES2026:reviver 增加了 context 参数
const parsed = JSON.parse(json, (key, value, context) => {
// context 提供了丰富的原始信息
// context.parent:包含原始未解析的父对象
// context.source:原始 JSON 字符串中对应的片段
// context.path:当前的 JSON Pointer 路径
console.log(`Key: "${key}", Path: ${context.pointer}`);
// 根据 key 名和父对象类型做条件转换
if (key === 'timestamp' && typeof context.parent === 'object') {
// 只有在事件对象内的 timestamp 才转为 Date
if (context.parent.type !== undefined) {
return new Date(value);
}
}
// 或者:直接使用原始片段做更复杂的解析
if (key === 'rawData' && context.source) {
// context.source 包含原始 JSON 中的这段内容
// 例如:'"rawData": "..."' 中的 "..." 部分
return JSON.parse(context.source);
}
return value;
});
7.3 JSON Pointer 路径的实用价值
// 场景:根据不同路径做不同处理
const config = JSON.stringify({
database: {
connection: {
host: 'localhost',
port: 5432,
credentials: {
username: 'admin',
// 这个 password 不应该出现在日志中
password: 'super-secret'
}
}
},
features: {
debugMode: false
}
});
const sanitized = JSON.parse(config, (key, value, context) => {
// 只在最终值(key 为空字符串)时输出
if (key === '') return value;
// 移除敏感字段
if (key === 'password') {
console.warn(`[Security] Password field detected at ${context.pointer}`);
return '[REDACTED]';
}
// 将端口号统一转为数字(如果仍是字符串形式)
if (key === 'port' && typeof value === 'string') {
return parseInt(value, 10);
}
return value;
});
八、JSON.rawJSON:精细控制 JSON.stringify 的输出
8.1 问题:无法保留原始 JSON 子串
JSON.stringify 的一个长期痛点是:你无法告诉它"这个值已经是 JSON 格式了,请原样输出"。当你处理已经序列化的子数据时,这个问题特别明显:
// 场景:构建一个 JSON,其中一部分已经是序列化的字符串
const embeddedJson = JSON.stringify({ nested: 'data' });
// embeddedJson = '{"nested":"data"}'
const result = JSON.stringify({
type: 'wrapper',
payload: embeddedJson, // 这里会被双重转义
count: 42
});
console.log(result);
// {"type":"wrapper","payload":"{\"nested\":\"data\"}","count":42}
// payload 字段被额外转义了!
// 如果下游代码 JSON.parse 它,payload 仍是一个字符串,不是对象
// 解决方案(ES2026之前):手动拼接字符串(危险)
const resultOld = '{"type":"wrapper","payload":' + embeddedJson + ',"count":42}';
// 问题:容易出错,破坏了 JSON.stringify 的安全保障
8.2 JSON.rawJSON 的解决方案
ES2026 引入了 JSON.rawJSON() 工厂函数:
// 创建一个"原始 JSON 标记对象"
const rawPayload = JSON.rawJSON('{"nested":"data"}');
console.log(rawPayload); // { [Symbol.rawJSON]: '{"nested":"data"}' }
// 现在用 JSON.stringify 处理它
const result = JSON.stringify({
type: 'wrapper',
payload: rawPayload, // 直接嵌入,不转义
count: 42
});
console.log(result);
// {"type":"wrapper","payload":{"nested":"data"},"count":42}
// ✅ payload 是对象,不是字符串!
// 验证:parse 回来
const parsed = JSON.parse(result);
console.log(parsed.payload); // { nested: 'data' } ✅
console.log(parsed.payload.nested); // 'data' ✅
8.3 高级用法:动态构建 rawJSON
// 场景:从外部来源获取已经序列化的数据
async function mergeWithExternalData(localData, externalJsonString) {
// externalJsonString 是来自第三方的 JSON 字符串
// 我们信任它的格式,直接嵌入
const merged = JSON.stringify({
local: localData,
external: JSON.rawJSON(externalJsonString),
timestamp: Date.now()
});
return merged;
}
// 场景:构建自定义语义的 JSON(避免双重序列化)
function createGeoJSON(coordinate) {
return JSON.stringify({
type: 'Point',
coordinates: JSON.rawJSON(`[${coordinate.lng},${coordinate.lat}]`)
});
}
const geoJson = createGeoJSON({ lng: 116.397128, lat: 39.916527 });
console.log(geoJson);
// {"type":"Point","coordinates":[116.397128,39.916527]}
// ✅ 坐标没有被引号包裹
8.4 rawJSON 的安全保证
JSON.rawJSON 并不是完全不受信任的——它内部会验证传入的字符串是否是合法的 JSON 片段:
// 传入非法 JSON 会抛出错误
try {
JSON.rawJSON('not valid json');
} catch (e) {
console.error(e); // SyntaxError: Invalid raw JSON value
}
// 这确保了使用 rawJSON 不会破坏最终的 JSON 格式
const safe = JSON.stringify({
data: JSON.rawJSON('{"trusted": true}') // 只信任已验证的数据
});
console.log(JSON.parse(safe).data.trusted); // true
九、兼容性与 Polyfill:什么时候能用上这些特性?
9.1 浏览器和 Node.js 支持情况
截至2026年7月,各主要运行时对 ES2026 特性的支持情况:
| 特性 | Node.js 24+ | Chrome 135+ | Firefox 135+ | Safari 25+ |
|---|---|---|---|---|
Math.sumPrecise | ✅ | ✅ | ✅ | ✅ |
Iterator.concat | ✅ | ✅ | ✅ | ✅ |
Array.fromAsync | ✅ | ✅ | ✅ | ✅ |
Error.isError | ✅ | ✅ | ✅ | ✅ |
Map.getOrElse | ✅ | ✅ | ✅ | ✅ |
Uint8Array 编码方法 | ✅ | ✅ | ✅ | ✅ |
| JSON.parse reviver 增强 | ✅ | ✅ | ✅ | ✅ |
JSON.rawJSON | ✅ | ✅ | ✅ | ✅ |
注:以上兼容性为基于 TC39 提案进展的合理推断,实际支持情况请以 caniuse.com 和 node.green 为准。
9.2 Polyfill 方案
对于暂时不支持的环境,可以使用 core-js 或专门的 polyfill:
// 安装 core-js 最新版
// npm install core-js@latest
import 'core-js/actual/math/sum-precise';
import 'core-js/actual/array/from-async';
import 'core-js/actual/array-buffer/uint8-array-base64';
import 'core-js/actual/json/raw-json';
// 或者按需导入特定特性
import 'core-js/actual/json/parse';
// JSON.parse 的增强 reviver 功能由 core-js 提供 polyfill
9.3 特性检测
// 特性检测函数
const ES2026Features = {
sumPrecise: () => typeof Math.sumPrecise === 'function',
iteratorConcat: () => typeof Iterator.concat === 'function',
fromAsync: () => typeof Array.fromAsync === 'function',
errorIsError: () => typeof Error.isError === 'function',
mapGetOrElse: () => {
const m = new Map();
return typeof m.getOrElse === 'function';
},
uint8ToHex: () => typeof new Uint8Array().toHexString === 'function',
rawJSON: () => typeof JSON.rawJSON === 'function',
enhancedReviver: () => {
try {
JSON.parse('{}', (k, v, ctx) => ctx && 'pointer' in ctx);
return true;
} catch { return false; }
}
};
// 使用示例
function requireFeature(feature, fallback) {
if (!ES2026Features[feature]()) {
console.warn(`Feature ${feature} not available, using fallback`);
return fallback;
}
return null; // 使用原生实现
}
十、总结:ES2026 的工程意义
10.1 这不是一个"表面更新"的版本
回顾 ES2026 的8大特性,我们可以发现一个清晰的趋势:ES2026 在填补 JavaScript 在工程实践中的关键空白。
| 类别 | 特性 | 解决的问题 |
|---|---|---|
| 数值计算 | Math.sumPrecise | 浮点累加精度问题 |
| 迭代器 | Iterator.concat | 多迭代器拼接的标准化 |
| 异步处理 | Array.fromAsync | 异步可迭代对象的统一转换 |
| 错误处理 | Error.isError | 跨 realm 错误识别 |
| 集合操作 | Map.getOrElse | 键缺失处理的优雅化 |
| 编码转换 | Uint8Array 编码方法 | 二进制↔字符串转换标准化 |
| JSON 解析 | 增强 reviver | 解析上下文的精确感知 |
| JSON 序列化 | JSON.rawJSON | 防止双重序列化问题 |
这些特性有一个共同特点:它们不是为了炫耀语言能力,而是为了减少我们对第三方库的依赖,让 JavaScript 本身变得更工程化。
10.2 对日常开发的影响
Math.sumPrecise:金融、电商、数据分析场景不再需要引入decimal.js或手写 Kahan 算法。Array.fromAsync:Fetch API 的 body、数据库游标、WebSocket 消息流都可以用统一的 API 处理。Uint8Array编码方法:Crypto API、WebSocket 二进制帧、文件处理的代码可以减少一大半的胶水代码。Error.isError:错误处理中间件和统一上报系统可以写得更加健壮。JSON.rawJSON:微服务间 JSON 消息拼接变得既安全又优雅。
10.3 展望:JavaScript 的工程成熟度持续提升
从 ES6 的 class、Promise、生成器,到 ES2017 的 async/await,再到 ES2026 的这些工程级 API,JavaScript 正在从一个"能用"的脚本语言,转变为一个"工程化"的主流编程语言。
这些 API 的出现让我们有理由相信:未来的 JavaScript 代码,将越来越少依赖 lodash、date-fns、query-string 等辅助库,越来越多地依赖语言本身的 primitives。
对于开发者而言,现在就开始了解这些新特性,并在 Node.js 24+ 或最新浏览器环境中实践它们,将在未来1-2年内获得明显的工程收益。
参考资料: