← Buffer.equals() 方法 Buffer.readIntBE() 方法 →

Node.js Buffer.subarray()方法解析:零拷贝切片与内存视图指南

著
原创 2026-10-15 Node.js 已有人查阅

1、从一个容易被误解的方法说起

Buffer.subarray()看起来很简单:给定start和end,返回一段字节。但它的返回值与普通“截取”有着本质区别——不分配新内存,不复制数据,返回的是父缓冲区同一块内存区域上的一个视图。

Node.js官方文档中明确写道:subarray()返回的新Buffer与原Buffer引用同一段内存,偏移量和裁剪由start和end决定;两个对象的内存区域是重叠的,修改其中一个会反映到另一个。

这意味着一行代码的疏忽,就可能导致数据在“不该被修改的地方”被意外改写。

2、核心机制:共享内存,而非复制

理解subarray()的关键在于区分“视图”与“副本”。

创建一个Buffer时,Node.js在底层分配了一块连续的内存区域。subarray(start,end)所做的只是生成一个新的Buffer对象,该对象的内部指针指向父缓冲区内存的start位置,长度由end-start决定。没有任何新的内存分配发生。

这个行为继承自TypedArray.prototype.subarray(),Node.js的Buffer本身就是Uint8Array的子类实例。

const parent = Buffer.from('Hello, World!');
const view = parent.subarray(7, 12);

// 修改视图
view.write('Universe');

console.log(parent.toString()); // 'Hello, Unive!'

parent的第7到第11字节被同步改写,因为view和parent指向同一段内存。

3、负数索引:从末尾计数的规则

start和end均接受负值,表示从缓冲区末尾开始计算。负索引的行为继承了TypedArray的语义:

const buf = Buffer.from('buffer');

buf.subarray(-6, -1).toString(); // 'buffe',等价于 subarray(0, 5)
buf.subarray(-6, -2).toString(); // 'buff',等价于 subarray(0, 4)
buf.subarray(-5, -2).toString(); // 'uff',等价于 subarray(1, 4)

规则:负索引=缓冲区长度+负数值。-1指向之后一个字节的后一位(end是排他的),这与slice()的索引语义一致。

4、版本演进:从slice()到subarray()的职责迁移

早期Node.js中,buf.slice()承担了类似的角色。但slice()的行为与JavaScript中其他slice()的惯例不一致,造成了认知负担。

对比三种slice的行为差异:

const buf = Buffer.from([1]);
buf.slice()[0] = 2;
console.log(buf[0]); // 2,原值被修改

const arr = [1];
arr.slice()[0] = 2;
console.log(arr[0]); // 1,原值不变

const u8 = new Uint8Array([1]);
u8.slice()[0] = 2;
console.log(u8[0]); // 1,原值不变

Buffer的slice()使用共享内存,而数组和TypedArray的slice()执行复制。这一不一致性导致了Buffer.prototype.slice()被标记为废弃(Stability:0),官方推荐迁移到subarray()。

值得注意的是,虽然slice()已废弃,但它与subarray()在当前版本中行为一致——都是共享内存视图。废弃的原因是命名语义与JavaScript生态的惯例冲突,而非功能缺陷。

5、与buf.slice()的职责边界

维度 subarray() slice()(已废弃)
内存行为 共享视图 共享视图
索引语义 与TypedArray一致 与Array的slice语义冲突
官方状态 推荐使用 Stability:0,废弃
类型兼容 继承自TypedArray 仅Buffer特有

选择建议明确:所有需要“零拷贝视图”的场景,使用subarray()。slice()保留仅为历史兼容,新代码不应使用。

6、常见误区速查

误区一:subarray返回的是“副本”。它返回的是视图。修改视图会修改父缓冲区。

误区二:负数索引从0开始计数。负索引中不存在“倒数第0个”。-1表示之后一个字节之后的边界位置。

误区三:subarray可以脱离父缓冲区独立使用。视图的生命周期依附于父缓冲区的底层内存。如果父缓冲区被垃圾回收,但视图仍被引用,底层内存不会被释放(视图持有引用)。

误区四:subarray比slice更快。在Node.js16.15.0之前,Buffer.prototype.subarray()的性能并不理想,部分库(如ws)曾使用内部FastBuffer来绕过这一开销。在当前版本中,这一差异已不再显著。

7、异常与边界处理

subarray()本身不会抛出异常。索引参数会被强制转换为整数。边界情况的处理规则:

  • start>end:返回空Buffer。

  • start<0且绝对值大于长度:从0开始。

  • end>buf.length:截取到缓冲区末尾。

  • 传入undefined:使用默认值(start为0,end为buf.length)。

const buf = Buffer.from('test');

buf.subarray(10, 20).length; // 0
buf.subarray(-100, 2).toString(); // 'te'
buf.subarray(2, 100).toString(); // 'st'

8、性能对比:subarray与Buffer.from的取舍

当需要保留数据副本时,Buffer.from(buf.subarray(a,b))会执行一次显式的内存复制。代价是额外的内存分配和字节拷贝,收益是视图与父缓冲区解耦。

对比两种模式的开销:

// 视图模式:零拷贝
const view = parent.subarray(0, 100);

// 副本模式:一次分配 + 一次拷贝
const copy = Buffer.from(parent.subarray(0, 100));

在频繁创建大量切片且父缓冲区生命周期较短的场景中,视图模式的收益显著。但若视图会被长期持有而父缓冲区本身很大,视图会阻止父缓冲区的内存释放,造成事实上的内存泄漏。此时副本模式是更安全的选择。

9、示例

示例一:二进制协议字段解析

const packet = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);

const version = packet.subarray(0, 1)[0];
const length  = packet.subarray(1, 3).readUInt16BE(0);
const payload = packet.subarray(3);

console.log({ version, length, payload: payload.toString('hex') });

示例二:流式数据分块处理

const fs = require('fs');
const stream = fs.createReadStream('data.bin');

let offset = 0;

stream.on('data', (chunk) => {
  // 创建视图,不复制 chunk 的内存
  const frame = chunk.subarray(0, Math.min(16, chunk.length));
  processFrame(frame, offset);
  offset += chunk.length;
});

示例三:Worker线程享缓冲区切片

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
  const data = Buffer.from('ABCDEFGHIJ'.repeat(100));
  const chunkSize = Math.ceil(data.length / 4);

  for (let i = 0; i < 4; i++) {
    const slice = data.subarray(i * chunkSize, (i + 1) * chunkSize);
    new Worker(__filename, { workerData: slice });
  }
} else {
  const result = workerData.toString('utf8').toUpperCase();
  console.log(result.length);
}

示例四:原地数据变换

const buf = Buffer.from('1234567890');

// 只变换第 2 到第 5 个字符为平方值
const segment = buf.subarray(1, 5);
for (let i = 0; i < segment.length; i++) {
  segment[i] = segment[i] * segment[i];
}

console.log(buf.toString()); // '1491627890'

10、延伸阅读与参考文献

Node.js官方文档—Buffer.subarray()
https://nodejs.org/api/buffer.html#bufsubarraystart-end
权威语法说明与版本标注,包含负数索引示例。

Node.js官方文档—缓冲区和TypedArray
https://nodejs.cn/api/buffer/typedarray.html
解释Buffer与TypedArray的继承关系及slice/subarray的语义差异。

Node.jsGitHubPR#28101
https://github.com/nodejs/node/pull/28101
Buffer#subarray()的文档补充与Uint8Array#slice()行为说明的讨论记录。

StackOverflow—Buffer.slicevssubarray
https://stackoverflow.com/questions/42919469/
对比slice废弃原因与subarray推荐用法的社区讨论。

Node.jsBuffer文档(v5.11.0历史版本)
https://nodejs.org/download/rc/v5.11.0-rc.1/docs/api/buffer.html
可用于追溯slice()在早期版本中的行为描述。

← Buffer.equals() 方法 Buffer.readIntBE() 方法 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号