← child_process 子进程模块 Stream 流处理 →

Node.js Buffer教程:Buffer.from、Buffer.alloc、编码转换与二进制数据处理指南

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

1.Buffer要解决什么问题

JavaScript早期只有字符串和数字,没有原生的二进制数据类型。浏览器里可以用ArrayBuffer和TypedArray处理二进制,但Node.js需要面对的是文件系统、TCP流、加密运算、图像处理这些场景,操作对象是字节序列而不是文本。Buffer就是为这个需求设计的。

Buffer的内存分配在V8堆之外。它底层是一段连续的原始内存,可以按字节读写,也可以和字符串之间做编码转换。因为不在V8堆内,Buffer不受堆大小限制的直接影响,处理大文件时比普通JavaScript对象更合适。

Buffer是全局类,不需要require('node:buffer')就能使用。但从Node.js的模块规范角度,显式引入node:buffer在阅读上更清晰,尤其是在大型项目里,能避免读者猜测Buffer来自哪里。

一个需要先建立的认知:Buffer存的是字节,不是字符。一个中文字符在UTF-8下占3个字节,一个英文字母占1个字节。buffer.length返回的是字节数,不是字符数。这个区别在计算文件大小、分片、截断字符串时经常导致错误。

2.Buffer.from:从已有数据创建

Buffer.from是最常用的创建方式,支持字符串、数组、ArrayBuffer、另一个Buffer。

const buf1 = Buffer.from('Hello, Node.js');
console.log(buf1);
console.log(buf1.toString());

const buf2 = Buffer.from([50, 55, 60, 65, 70]);
console.log(buf2);
console.log(buf2.toString());

const buf3 = Buffer.from(buf2);
console.log(buf3);

输出:

<Buffer 48 65 6c 6c 6f 2c 20 4e 6f 64 65 2e 6a 73>
Hello, Node.js
<Buffer 32 37 3c 41 46>
27<AF
<Buffer 32 37 3c 41 46>

第一段把字符串按UTF-8编码成字节。第二段用整数数组创建,50对应字符2,55对应7,60对应<,65对应A,70对应F,所以toString输出27<AF。第三段从已有Buffer复制一份新内存。

从字符串创建时可以指定编码:

Buffer.from('hello', 'utf8');
Buffer.from('aGVsbG8=', 'base64');
Buffer.from('68656c6c6f', 'hex');

Base64和Hex在传输和存储场景常见。JWT的payload是Base64URL编码,文件校验值常用Hex表示。

从ArrayBuffer创建时,Buffer.from(arrayBuffer)会共享同一段内存,不是复制。修改Buffer会影响原ArrayBuffer,反之亦然。需要独立副本时用Buffer.from(arrayBuffer.slice(0))或者Buffer.copy。

3.Buffer.alloc与Buffer.allocUnsafe

Buffer.alloc(size)分配指定字节数并用零填充。

const buf = Buffer.alloc(20);
console.log(buf);

输出:

<Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>

Buffer.allocUnsafe(size)分配内存但不初始化,速度更快,内容是内存里残留的数据。

const buf = Buffer.allocUnsafe(5);
console.log(buf);

buf.fill(0);
console.log(buf);

输出示例:

<Buffer 3c 45 a2 01 7f>
<Buffer 00 00 00 00 00>

第一次输出每次运行都可能不同,因为读的是未清理的内存。如果这段内存之前存过密钥、token、用户数据,就可能泄露。

选择逻辑:不确定内容是否会被完整覆盖时,用Buffer.alloc。只有在分配后立即完整写入、且性能敏感的路径上才考虑allocUnsafe。Node.js内部在流处理、网络包解析里会用allocUnsafe,因为后续操作会覆盖全部字节。业务代码里,除非有明确的性能压测数据支持,否则不必要冒这个风险。

项目经验:在文件上传的中间件里,我曾经见过用allocUnsafe分配缓冲区但只写入了部分数据,剩余字节来自上一次请求。后来改成alloc并加了一层断言校验,问题消失。这类bug不容易复现,排查成本高。

4.编码转换

Buffer和字符串之间的转换依赖编码。Node.js支持的编码包括utf8、utf16le、latin1、base64、base64url、hex、ascii、binary、ucs2。

const text = '你好,Node.js';

const utf8Buf = Buffer.from(text, 'utf8');
const base64Str = utf8Buf.toString('base64');
const hexStr = utf8Buf.toString('hex');

console.log('UTF-8 字节数:', utf8Buf.length);
console.log('Base64:', base64Str);
console.log('Hex:', hexStr);

const restored = Buffer.from(base64Str, 'base64').toString('utf8');
console.log('还原:', restored);

输出:

UTF-8 字节数: 15
Base64: 5L2g5aW977yMTm9kZS5qcw==
Hex: e4bda0e5a5bdefbc8c4e6f64652e6a73
还原: 你好,Node.js

Base64把二进制转成只含可打印字符的字符串,适合放进JSON、URL、HTTP头部。Hex每个字节用两个十六进制字符表示,适合做摘要展示。UTF-8是默认编码,处理文本时不需要显式指定。

一个容易出错的点:用错误的编码解码。比如把UTF-8字节按latin1解码,中文会变成乱码。反过来,如果数据本身是latin1,用UTF-8解码也会出错。编码必须和写入时一致。

Buffer.byteLength(string,encoding)返回字符串在指定编码下的字节数,比string.length更准确。

console.log('你好'.length);
console.log(Buffer.byteLength('你好', 'utf8'));

输出是2和6。字符数是2,字节数是6。做分片或限制请求体大小时,应该用byteLength。

5.常用方法与实践

concat合并多个Buffer:

const a = Buffer.from('Hello ');
const b = Buffer.from('Node.js');
const merged = Buffer.concat([a, b]);
console.log(merged.toString());

输出HelloNode.js。第二个参数可以指定总长度,用于预分配和截断。

slice与subarray返回共享内存的视图:

const buf = Buffer.from('Hello, Node.js');
const part = buf.subarray(0, 5);
part[0] = 0x4a; // 'J'
console.log(buf.toString()); // Jello, Node.js

subarray不复制数据,修改会影响原Buffer。需要独立副本用Buffer.from(buf.subarray(0,5))。slice在Node.js较新版本里行为与subarray一致。

copy把数据复制到目标Buffer:

const source = Buffer.from('Hello');
const target = Buffer.alloc(5);
source.copy(target, 0, 0, source.length);
console.log(target.toString());

equals与compare用于比较:

const x = Buffer.from('abc');
const y = Buffer.from('abc');
console.log(x.equals(y)); // true
console.log(Buffer.compare(x, y)); // 0

compare按字典序返回负数、零、正数,适合排序。

indexOf、includes用于查找:

const buf = Buffer.from('Hello, Node.js');
console.log(buf.indexOf('Node')); // 7
console.log(buf.includes('Node')); // true

write与read系列用于结构化数据。处理二进制协议时经常用到:

const buf = Buffer.alloc(8);
buf.writeUInt32BE(0x12345678, 0);
buf.writeUInt32LE(0x12345678, 4);
console.log(buf);

大端和小端的区分在网络协议、文件格式解析里很关键。同一个数值在不同字节序下解析结果不同,读错端序会得到错误的值。

6.Buffer与ArrayBuffer的区别

两者都是二进制数据容器,但设计目标和API不同。

Buffer是Node.js特有的类,继承自Uint8Array,提供了字符串编码、读写整数、拼接等便利方法。ArrayBuffer是ECMAScript标准的一部分,浏览器和Node.js都有,表示一段固定大小的原始内存,本身不能直接读写,需要通过TypedArray或DataView操作。

特性 Buffer ArrayBuffer
定义 Node.js二进制数据类 标准原始内存块
环境 Node.js 浏览器与Node.js
数据访问 直接读写,内置编码方法 通过TypedArray或DataView
编码支持 utf8、base64、hex等 不直接提供
典型用途 文件、流、网络、加密 底层内存操作、跨平台数据传输

在WebAPI与Node.js之间传递数据时,经常需要转换:

const buf = Buffer.from('hello');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);

反过来:

const ab = new ArrayBuffer(5);
const buf = Buffer.from(ab);

buf.buffer返回底层ArrayBuffer,但可能比Buffer本身大,因为Buffer是视图。所以切片时要加上byteOffset。

7.项目实例:图片上传校验

假设一个接口接收图片上传,需要校验文件头判断真实类型,而不是相信客户端传来的Content-Type。

function detectImageType(buffer) {
  if (buffer.length < 4) return 'unknown';

  const hex = buffer.subarray(0, 4).toString('hex');

  if (hex.startsWith('ffd8ff')) return 'jpeg';
  if (hex.startsWith('89504e47')) return 'png';
  if (hex.startsWith('47494638')) return 'gif';
  if (hex.startsWith('52494646')) return 'webp';

  return 'unknown';
}

const buf = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
console.log(detectImageType(buf));

输出jpeg。JPEG文件头是FFD8FF,PNG是89504E47,GIF是47494638。通过读取前几个字节判断格式,比信任客户端声明的类型可靠。

实际项目里还要限制文件大小,在流式读取时累计字节数,超过阈值就中断。Buffer在这里的角色是承载已读取的片段,配合流处理避免一次性把整个文件读进内存。

8.流与Buffer的配合

Node.js的流默认以Buffer形式传输数据。data事件拿到的就是Buffer。

const fs = require('node:fs');

const stream = fs.createReadStream('large-file.bin');

let total = 0;
stream.on('data', (chunk) => {
  total += chunk.length;
});

stream.on('end', () => {
  console.log('总字节数:', total);
});

chunk是Buffer,chunk.length是字节数。如果处理文本,需要chunk.toString('utf8'),但要注意一个多字节字符可能被切在两个chunk之间,直接逐块toString会产生乱码。正确做法是用StringDecoder或设置流的编码。

const { StringDecoder } = require('node:string_decoder');
const decoder = new StringDecoder('utf8');

stream.on('data', (chunk) => {
  process.stdout.write(decoder.write(chunk));
});

StringDecoder会缓存不完整的字节序列,等下一个chunk到达再拼接。这是处理文本流的常见模式。

9.现在框架与库中的用法

Express的express.raw()中间件把请求体解析成Buffer,用于webhook签名校验。Stripe、GitHub的回调都需要原始字节来做HMAC验证,JSON解析会改变字节顺序,所以必须用raw。

数据库驱动(mysql2、pg)返回的BLOB、BYTEA字段是Buffer。处理时需要显式调用toString或写入文件。

加密模块的输出和输入都是Buffer。crypto.randomBytes()返回Buffer,cipher.update()返回Buffer,hash.digest()不带参数时返回Buffer。

WebSocket库的二进制帧对应Buffer。自定义协议解析时,通常按固定长度读取字段,用readUInt16BE、readUInt32BE等方法。

10.本节课程知识要点

  • Buffer存储原始字节,内存分配在V8堆之外。

  • Buffer.from从字符串、数组、ArrayBuffer创建;Buffer.alloc零填充;allocUnsafe不初始化。

  • 不确定内容是否被完整覆盖时,使用Buffer.alloc。

  • buffer.length是字节数,不是字符数。

  • 编码转换必须读写一致,UTF-8、Base64、Hex是最常用的三种。

  • slice和subarray共享内存,需要副本时用Buffer.from。

  • Buffer与ArrayBuffer转换时注意byteOffset和byteLength。

  • 流处理文本时用StringDecoder避免多字节字符被截断。

  • 文件类型校验读文件头字节,比信任Content-Type可靠。

11.练习与思考

  1. 用Buffer.from和Buffer.alloc分别创建10字节的Buffer,比较初始内容差异。

  2. 把一段中文分别按UTF-8、Base64、Hex编码,再还原,验证结果一致。

  3. 用subarray切出一个Buffer的子视图,修改子视图的第一个字节,观察原Buffer是否变化。

  4. 写一个函数,接收Buffer,判断它是否是PNG文件,并返回宽度和高度(提示:PNG的宽高在IHDR块中,偏移16和20字节处,大端32位整数)。

  5. 用流读取一个文本文件,分别用直接toString和StringDecoder处理,观察多字节字符边界处的差异。

Buffer是Node.js处理二进制数据的基础设施。文件、网络、加密、流,这些场景的数据最终都以Buffer形式流转。掌握创建方式的安全差异、编码转换的一致性、共享内存与副本的区别,能避开不少隐蔽的bug。实际项目里,Buffer很少单独出现,它总是和流、加密、协议解析、文件系统配合使用,理解它在这些组合中的角色,比记住API列表更有意义。

← Node.js child_process子进程模块教程:exec、execFile、spawn、fork与IPC通信指南 Node.js Stream流处理:Readable、Writable、Duplex、Transform、pipe管道与背压处理指南 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号