编程 ECMAScript 2026 深度实战:第17版规范的 8 大新特性从入门到精通——从高精度求和到 JSON 精细控制

2026-07-12 13:16:13 +0800 CST views 16

ECMAScript 2026 深度实战:第17版规范全面解析——从高精度求和到 JSON 精细控制,8 大新特性重塑 JavaScript 开发范式

前言:ECMAScript 2026 来了,但为什么我们还需要关心它?

2026年6月30日,ECMA International 正式批准了 ECMAScript 2026(ECMA-262 第17版)。每一年这个时候,我们都会听到"JS又更新了"的声音,但大多数开发者可能只是扫一眼新闻标题,然后继续写自己的业务代码——毕竟,constlet 已经够用了,Promiseasync/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 求和吗?我自己写一个函数也行啊。

但关键区别在于:

  1. 引擎级优化:内置 API 可以被 JavaScript 引擎直接优化,在 V8 / SpiderMonkey / JavaScriptCore 层面可能有 SIMD 加速或更精确的浮点中间表示,第三方库做不到这一点。
  2. 跨环境一致行为:各引擎对 Math.sumPrecise 的实现必须遵循规范定义的行为,而自己写的 Kahan 实现可能在不同引擎的浮点优化策略下行为略有差异。
  3. 标准互操作性:一旦 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.concatIterator.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 为 MapWeakMap 新增了 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

WeakMapgetOrElse 特别有价值,因为它要求键是对象引用,没有 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.comnode.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年内获得明显的工程收益。


参考资料

推荐文章

最全面的 `history` 命令指南
2024-11-18 21:32:45 +0800 CST
Golang - 使用 GoFakeIt 生成 Mock 数据
2024-11-18 15:51:22 +0800 CST
Web 端 Office 文件预览工具库
2024-11-18 22:19:16 +0800 CST
程序员茄子在线接单