前端知库

Buffer 是什么?Buffer.alloc、Buffer.from 和 Uint8Array 有什么关系?

Buffer 是 Node.js 里表示一段原始字节的类,它是 Uint8Array 的子类,额外提供编码转换、读写整数等方法,Node.js API 接受 Buffer 的地方也接受普通 Uint8Array。Buffer.alloc 分配并清零,Buffer.allocUnsafe 不清零、可能带着旧数据但更快;Buffer.from 按字符串、数组或 ArrayBuffer 创建,传 ArrayBuffer 时共享内存不复制。Buffer 的 slice 不复制,和 Uint8Array 的 slice 相反,已被废弃,应改用 subarray。

参考答案

简答

读文件、收网络数据时拿到的是一串字节,Node.js 用 Buffer 表示这串字节。Buffer.from('你好') 得到 6 个字节,因为默认按 UTF-8 编码,每个汉字占 3 个字节。

Buffer 是 Uint8Array 的子类,所以下标读写、length、subarray 这些 TypedArray 的东西它都有,另外加了字符串和 hex、base64 之间的编码转换,以及按字节序读写整数这些方法。Node.js 文档说,接受 Buffer 的 API 也接受普通的 Uint8Array。

创建方式要分清。Buffer.alloc(n) 分配并清零;Buffer.allocUnsafe(n) 不清零,更快,但里面可能是旧数据,甚至是敏感数据,必须马上全部写满;Buffer.from() 从字符串、数组复制出新内存,传 ArrayBuffer 时则共享那块内存,不复制。

最容易踩的坑是 slice。Uint8Array 的 slice 会复制,Buffer 的 slice 却不复制,改了切片会改到原数据。这个方法已经被废弃,统一用 subarray 表示“不复制的视图”,需要复制时再显式复制。

Buffer 和 Uint8Array 是什么关系

从 Node.js 3.0 开始,Buffer 类继承自 Uint8Array。所以 buf instanceof Uint8Array 是 true,TypedArray 的方法和属性在 Buffer 上都能用。ArrayBuffer、视图、subarray 这些基础概念见《ArrayBuffer、TypedArray 和普通数组有什么区别?》。

Buffer 多出来的主要是编码相关的能力。下面这段要在 Node.js 里运行,用 Node.js 22.23.3 验证:

JavaScript
const buf = Buffer.from('你好');
console.log(buf.length); // 6
console.log(buf.toString('hex')); // e4bda0e5a5bd
console.log(buf.toString('base64')); // 5L2g5aW9
console.log(buf instanceof Uint8Array); // true
console.log(buf.subarray(0, 3).toString()); // 你
console.log(buf.subarray(0, 2).toString()); // �

length 是字节数,不是字符数,所以是 6。toString 默认按 UTF-8 解码,前 3 个字节正好是“你”。只截前 2 个字节时,这不是一段完整的 UTF-8,文档说无效的字节会被换成替换字符 U+FFFD,也就是最后一行的 �。所以按字节切分流数据再解码时,要处理一个字符被切在两块之间的情况。

Node.js 文档也说,Buffer 虽然在全局作用域里能直接用,仍然建议显式写 const { Buffer } = require('node:buffer') 或对应的 import。

alloc 和 allocUnsafe 差在哪里

Buffer.alloc(size) 返回一块已经填好 0 的内存。Buffer.allocUnsafe(size) 不做初始化,文档的原话是内容未知,可能包含敏感数据,要用 buf.fill(0) 初始化,或者确保马上把它全部覆盖。

allocUnsafe 快的原因有两个。一是省掉了清零。二是小块内存走内部的预分配池:Node.js 预先分配一块大小为 Buffer.poolSize 的 Buffer,allocUnsafe、Buffer.from(array)、Buffer.from(string)、Buffer.concat() 申请的大小小于 Buffer.poolSize >>> 1 时,从这块池子里切一段出来,减少垃圾回收的开销。Buffer.poolSize 的默认值在 Node.js 26.3 从 8192 提高到了 65536。

Buffer.alloc(size, fill) 永远不走这个池,Buffer.allocUnsafe(size).fill(fill) 在大小不超过池的一半时会走。所以两者结果一样,内存来源不同。

池化带来一个现象:小 Buffer 的 .buffer 不是只属于它的内存。在 Node.js 22.23.3 里,Buffer.from('hi').buffer.byteLength 是 8192,byteOffset 是 8,说明它只是池里的一段。把 buf.buffer 整个交给别的 API 时,必须带上 byteOffset 和 length,不然会读到池里别人的数据。文档也提醒,需要长期保留一小块池内存时,可以用 Buffer.allocUnsafeSlow() 分配不走池的内存再复制过去,避免整块池因为这一小段一直不能回收。

Buffer.from 什么时候复制,什么时候共享

Buffer.from(string) 按编码把字符串转成字节,默认 UTF-8。Buffer.from(array) 把一个整数数组复制成字节。这两种都会得到新内存。

Buffer.from(arrayBuffer, byteOffset, length) 不一样。文档说它创建的是 ArrayBuffer 的一个视图,不复制底层内存,行为和 new Uint8Array(arrayBuffer) 一样。所以拿 TypedArray 的 .buffer 去创建 Buffer,两边改的是同一块内存。

反过来,把一个 Buffer 传给 TypedArray 构造函数,比如 new Uint32Array(buf),会把每个字节当成一个整数复制过去,而不是把字节重新解释成 32 位整数。要按其他类型解读同一块内存,应该传 buf.buffer、buf.byteOffset 和长度。

slice 为什么不能和 Uint8Array 混着理解

先看 Uint8Array 自己的行为。subarray 返回共享内存的视图,slice 返回一份复制:

JavaScript
const bytes = new Uint8Array([104, 105, 33]);
const view = bytes.subarray(0, 2);
const copy = bytes.slice(0, 2);
bytes[0] = 72;
console.log(view[0]); // 72
console.log(copy[0]); // 104
console.log(view.buffer === bytes.buffer); // true
console.log(copy.buffer === bytes.buffer); // false

改了原数组的第 0 个字节,view 跟着变成 72,copy 还是 104。

可是 Buffer.prototype.slice() 和 subarray 一样,返回的是不复制的视图。文档说这种行为只是为了兼容旧代码而保留的,容易让人意外,从 Node.js 17.5、16.15 起已经废弃,推荐在 Buffer 和其他 TypedArray 上都用 subarray。所以在 Node.js 代码里看到 buf.slice(),要意识到它改的是原数据;需要独立副本时,用 Buffer.from(buf.subarray(a, b)) 或 Uint8Array.prototype.slice.call(buf, a, b) 显式复制。

除了 slice,文档还列了两处不兼容:buf.toString() 和 TypedArray 的 toString 不一样,前者按编码解码,后者输出逗号分隔的数字;buf.indexOf() 等方法多支持了一些参数,比如可以直接搜索字符串。

常见追问

为什么不再用 new Buffer()?

new Buffer() 的行为取决于第一个参数的类型。文档举的例子是,本该收到字符串 "100" 的接口被传进数字 100,就会变成分配 100 个字节。Node.js 8.0 之前这块内存不清零,可能泄露内存里的旧数据;8.0 之后虽然清零了,攻击者仍能让服务器分配超大内存。所以各种形式的 new Buffer() 都被废弃了,换成意图明确的 Buffer.from()、Buffer.alloc() 和 Buffer.allocUnsafe()。

写跨 Node.js 和浏览器的库,该用 Buffer 还是 Uint8Array?

公共接口用 Uint8Array 更通用,因为浏览器没有 Buffer,而 Node.js 的 API 本来就接受 Uint8Array,Buffer 传进来也是合法的 Uint8Array。字符串和字节的转换可以用 TextEncoder、TextDecoder,Base64 的写法见《Uint8Array.fromBase64/toBase64 和 btoa/atob 有什么区别?中文怎样转 Base64?》。

读大文件时应该一次读成一个 Buffer 吗?

不应该。一次读进来,整个文件都要放在内存里。大文件应该用流按块处理,每块是一个 Buffer,见《流和背压为什么能避免一次性读入大文件?》。