TC39 2025:Import Bytes、Iterator Chunking 和那些即将落地的新特性
写跨平台的 JS 代码时,读个二进制文件都得写三套逻辑:
javascript
// 浏览器
const bytes = await fetch('./photo.png').then(r => r.arrayBuffer());
// Node.js
const bytes = require('fs').readFileSync('./photo.png');
// Deno
const bytes = await Deno.readFile('./photo.png');
同样的需求,三种写法。想写个同构的图片处理库?先把这三套 API 适配一遍再说。
好消息是,TC39 在 2025 年推进了好几个提案来解决这类问题。这篇文章聊聊其中最值得关注的几个:Import Bytes、Iterator Chunking,以及今年已经进入 Stage 4 的新特性。
Import Bytes:一行代码搞定二进制导入
现在是什么状态
Stage 2.7(截至 2025 年 9 月),离正式标准就差临门一脚了。提案负责人是 Steven Salat,Guy Bedford 是共同作者。
核心语法
javascript
import bytes from "./photo.png" with { type: "bytes" };
// bytes 是 Uint8Array,底层是不可变的 ArrayBuffer
动态导入也支持:
javascript
const bytes = await import("./photo.png", { with: { type: "bytes" } });
就这么简单。不管你在浏览器、Node.js 还是 Deno,同一行代码,同样的结果。
为什么返回 Uint8Array 而不是 ArrayBuffer
提案选择返回 Uint8Array 而不是裸的 ArrayBuffer,理由挺实在的:
- 少一步操作 - 拿到 ArrayBuffer 你还得自己创建 TypedView,Uint8Array 直接就能用
- 跟现有 API 保持一致 -
Response.bytes()和Blob.bytes()都返回 Uint8Array - Node.js Buffer 兼容 - Buffer 本身就是 Uint8Array 的子类
为什么底层是不可变的 ArrayBuffer
这个设计决定挺有意思的。底层 ArrayBuffer 被设计成不可变的,原因有三:
- 避免共享可变状态 - 多个模块导入同一个文件,拿到的是同一个对象。如果可变,一个模块改了数据,其他模块全受影响
- 嵌入式场景 - 不可变数据可以直接放 ROM 里
- 安全性考虑 - 防止模块间通过共享 buffer 建立隐蔽通信通道
实际能干什么
图片处理:
javascript
import imageBytes from "./logo.png" with { type: "bytes" };
// 用 satori 之类的同构库处理
processImage(imageBytes);
加载字体:
javascript
import fontBytes from "./custom.woff" with { type: "bytes" };
// Canvas 或 PDF 生成时用
registerFont(fontBytes);
机器学习模型:
javascript
import modelBytes from "./model.bin" with { type: "bytes" };
loadModel(modelBytes);
工具链支持
好消息是,主流工具已经在跟进了。Deno 2.4、Bun 1.1.7 都有类似实现,Webpack、esbuild、Parcel 也支持类似的二进制导入机制。等提案正式落地,统一语法只是时间问题。
Iterator Chunking:迭代器分块终于有原生方案了
现在是什么状态
Stage 2.7(截至 2025 年 9 月),由 Michael Ficarra 主导。
两个核心方法
chunks(size) - 非重叠分块
javascript
const numbers = [1, 2, 3, 4, 5, 6, 7].values();
const chunked = numbers.chunks(3);
for (const chunk of chunked) {
console.log(chunk);
}
// [1, 2, 3]
// [4, 5, 6]
// [7]
windows(size) - 滑动窗口
javascript
const numbers = [1, 2, 3, 4].values();
const windowed = numbers.windows(2);
for (const window of windowed) {
console.log(window);
}
// [1, 2]
// [2, 3]
// [3, 4]
区别很直观:chunks 是切成一块一块互不重叠,windows 是滑动窗口每次移动一格。
解决什么问题
以前想做分块操作,要么自己写,要么引入 lodash:
javascript
// lodash 方案
import chunk from 'lodash/chunk';
const chunks = chunk([1, 2, 3, 4], 2);
// 原生方案
const chunks = [1, 2, 3, 4].values().chunks(2);
原生方案的优势:
- 不用装依赖
- 惰性求值,内存友好
- 跟整个迭代器生态无缝衔接
- 支持异步迭代器
实际场景
批量 API 请求:
javascript
async function batchProcess(items) {
const batches = items.values().chunks(50);
for (const batch of batches) {
await Promise.all(batch.map(item => api.process(item)));
await sleep(1000); // 避免触发限流
}
}
移动平均计算:
javascript
function movingAverage(numbers, windowSize) {
return numbers
.values()
.windows(windowSize)
.map(w => w.reduce((a, b) => a + b) / windowSize)
.toArray();
}
const prices = [100, 102, 98, 105, 103, 107];
const ma3 = movingAverage(prices, 3);
// 3日移动平均
N-gram 生成:
javascript
function generateNGrams(text, n) {
const words = text.split(' ');
return words.values()
.windows(n)
.map(w => w.join(' '))
.toArray();
}
const bigrams = generateNGrams("The quick brown fox", 2);
// ["The quick", "quick brown", "brown fox"]
边界情况的讨论
这个提案在推进过程中遇到了一个有意思的问题:如果迭代器元素少于窗口大小,windows() 应该返回什么?
javascript
const small = [1, 2].values();
const result = small.windows(3); // 只有2个元素,请求3个的窗口
// 选项1:不返回任何窗口
// 选项2:返回 [1, 2] 作为不完整窗口
委员会讨论后认为两种场景都有合理的使用需求,所以决定把 windows() 拆分成多个方法来分别处理这两种情况。这也是提案从 Stage 2 到 Stage 2.7 花了点时间的原因。
2025 年进入 Stage 4 的特性
除了上面两个还在推进的提案,2025 年还有好几个特性已经正式"毕业"了:
RegExp.escape(2 月)
安全转义正则表达式字符串,防止注入:
javascript
const userInput = "user@example.com (admin)";
const safePattern = RegExp.escape(userInput);
const regex = new RegExp(safePattern);
// 不用担心括号被解析成分组了
这个需求太常见了,以前都得自己写转义函数或者用第三方库。
Float16Array(2 月)
半精度浮点数的 TypedArray:
javascript
const f16Array = new Float16Array([1.5, 2.7, 3.1]);
主要面向机器学习和图形处理场景。模型权重经常用 fp16 存储,有了原生支持就不用自己做转换了。
Error.isError(5 月)
可靠地判断一个值是不是 Error:
javascript
if (Error.isError(value)) {
console.log(value.message);
}
为什么不用 instanceof Error?因为跨 realm(比如 iframe 或 Node.js 的 vm 模块)的 Error 实例会被判成 false。这个方法解决了这个历史问题。
Math.sumPrecise(7 月)
高精度求和:
javascript
const sum = Math.sumPrecise([0.1, 0.2, 0.3]);
// 比普通累加更精确,减少浮点误差累积
做金融计算或科学计算的应该会喜欢这个。
Uint8Array Base64 编解码(7 月)
原生的 Base64 编解码:
javascript
const bytes = Uint8Array.fromBase64('SGVsbG8=');
const base64 = bytes.toBase64();
// 还有 fromHex() 和 toHex()
终于不用为了 Base64 转换去找第三方库了。
Explicit Resource Management(已 Stage 4)
using 关键字,自动资源清理:
javascript
using file = await openFile('data.txt');
// 离开作用域自动关闭,不用手动 finally
借鉴了 Python 的 with 和 C# 的 using,解决了 JS 里资源管理一直很混乱的问题。
还有几个值得关注的 Stage 2 提案
Seeded PRNG(5 月进入 Stage 2)
可种子化的随机数生成器:
javascript
const random = new Random(12345); // 种子
const value = random.next();
// 同样的种子,同样的序列
游戏开发、测试、仿真这些场景经常需要可重现的随机序列。
Error Stack Accessor(5 月进入 Stage 2)
标准化错误堆栈的访问方式。现在各个引擎的 error.stack 格式都不一样,这个提案要统一它。
提案流程简单回顾
TC39 的提案分 5 个阶段:
- Stage 0:想法
- Stage 1:正式提案,开始讨论
- Stage 2:规范草案,API 基本稳定
- Stage 2.7:规范文本接近完成,准备写测试
- Stage 3:等待实现反馈
- Stage 4:正式纳入标准
Import Bytes 和 Iterator Chunking 都到了 Stage 2.7,离 Stage 3 就差 test262 测试和浏览器实现承诺了。
总结
2025 年 TC39 的进展还是挺给力的:
- Import Bytes 解决了跨平台二进制导入的老大难问题,同构库开发终于能省心了
- Iterator Chunking 补上了迭代器工具链的空白,chunks 和 windows 覆盖了大部分分块场景
- 一堆特性进入 Stage 4:RegExp.escape、Float16Array、Math.sumPrecise、Base64 编解码、资源管理...
这些特性有的已经可以通过 Babel 或 TypeScript 提前尝鲜了。如果你在用 Deno 或 Bun,Import Bytes 类似的功能现在就能用。
顺手安利几个我的开源项目:
Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB