Node.js buffer 模块
Node.js events 模块 讲完事件,还有一个基础概念没讲。文件内容、网络传输在底层都是二进制字节,JS 原生字符串处理不了这些字节流。这一篇解决核心问题,Buffer 是什么,怎么用它处理二进制数据。buffer 模块是全局的,不需要 require 就能直接用。
Buffer 是什么
Buffer 是在 V8 堆内存之外分配的原始二进制数据,本质是 Uint8Array 的子类,所以数组的方法它基本都有。
// 不需要 require,全局可用
const buf = Buffer.from('hello');
console.log(buf);
// 输出 <Buffer 68 65 6c 6c 6f>
每个数字代表一个字节,68 是字母 h 的十六进制 ASCII 码,65 是 e,以此类推。之前 Node.js fs 模块 里不传 encoding 读出来的就是这种对象。
为什么需要 Buffer
JS 字符串用的是 UTF-16 编码,字符长度和字节数对不上。中文”你”在 UTF-8 下占 3 个字节,但字符串长度是 1,字符串天然不适合表示字节流。
而文件系统和网络协议都是按字节工作的。读图片、传文件、做加密,到处是字节流,所以必须有专门的数据类型,这就是 Buffer 存在的理由。
创建 Buffer
Buffer.from 从字符串创建
// 第二个参数是源字符串的编码,默认 utf8
const a = Buffer.from('hello');
const b = Buffer.from('你好');
console.log(a.length, b.length);
// 输出 5 6,中文一个字占 3 字节
Buffer.from 从数组创建
const c = Buffer.from([104, 101, 108, 108, 111]);
console.log(c.toString());
// 输出 hello
Buffer.alloc 分配固定大小
// 分配 10 字节,用 0 填充
const d = Buffer.alloc(10);
console.log(d);
// 输出 <Buffer 00 00 00 00 00 00 00 00 00 00>
// 指定填充值,65 是字母 A 的 ASCII 码
const e = Buffer.alloc(10, 65);
console.log(e.toString());
// 输出 AAAAAAAAAA
Buffer.alloc 会先清空内存再分配,安全。还有个不安全的 Buffer.allocUnsafe,速度快但不清理旧内存,可能读到残留数据,新手一律用 alloc 就好。
转换 toString
toString 可以指定输出编码。
const buf = Buffer.from('你好 Node.js');
console.log(buf.toString('utf8'));
// 你好 Node.js
console.log(buf.toString('hex'));
// e4bda0e5a5bd204e6f64652e6a73 十六进制
console.log(buf.toString('base64'));
// 5L2g5aW9IE5vZGUuanM=
三种编码的典型场景。utf8 是常规文本。hex 常用于展示和存储字节,比如日志里记录二进制内容。base64 用于把二进制塞进文本场景,比如图片转 base64 后放进 JSON 或 HTML 里传输。
Buffer 与 ArrayBuffer 与 Uint8Array
| 类型 | 内存位置 | 特点 |
|---|---|---|
| Buffer | V8 堆外 | Node.js 专用,带编码转换等便捷方法 |
| ArrayBuffer | 堆外原始内存 | 只存字节,没有任何读写方法 |
| Uint8Array | 视情况 | 标准 JS 类型化数组,Buffer 的父类 |
三者的关系是,ArrayBuffer 是底层原始内存,Uint8Array 是它的视图,Buffer 是 Uint8Array 的子类,加上了 Node.js 生态的便捷方法。日常开发用 Buffer 就够了。
注意点
Buffer 内容可变
const buf = Buffer.from('hello');
// 直接按下标改字节,72 是 H 的 ASCII 码
buf[0] = 72;
console.log(buf.toString());
// 输出 Hello
Buffer 和字符串不一样,内容可以原地修改,下标直接对应字节。
slice 共享内存
const buf = Buffer.from('hello world');
const part = buf.slice(0, 5);
// 修改切片,原 Buffer 也跟着变
part[0] = 72;
console.log(buf.toString());
// 输出 Hello world
slice 返回的是共享同一块内存的视图,不是拷贝。改切片等于改原数据,想要独立副本要用 Buffer.from(part)。
实践
读一张图片转成 base64 字符串,再把 base64 转回 Buffer 写回新文件,对比两个文件验证一致。
// imgBase64.js
const fs = require('fs').promises;
async function main() {
// 1. 读图片,不传 encoding 得到 Buffer
const img = await fs.readFile('photo.png');
// 2. Buffer 转 base64 字符串
const base64 = img.toString('base64');
console.log('图片大小', img.length, '字节');
console.log('base64 长度', base64.length, '字符');
// 3. base64 转回 Buffer
const restored = Buffer.from(base64, 'base64');
// 4. 写回新文件
await fs.writeFile('photo_copy.png', restored);
// 5. 对比两个文件是否一致
const copy = await fs.readFile('photo_copy.png');
const same = img.equals(copy);
console.log('两个文件一致', same);
}
main().catch(err => console.error('失败', err.message));
准备一张图片并运行,没有图片时先用命令生成一个测试文件。
# 生成 100 字节的测试文件
node -e "require('fs').writeFileSync('photo.png', Buffer.alloc(100, 65))"
node imgBase64.js
输出类似。
图片大小 100 字节
base64 长度 136 字符
两个文件一致 true
Buffer.equals 逐字节比较两个 Buffer 是否相同。base64 的长度会比原字节多出约三分之一,这是 base64 编码本身的开销,每 3 个字节被拆成 4 个字符表示。
到这里异步编程的前置基础都齐了,回调、事件、二进制都有了。下一步进入异步的核心难点,Node.js 回调与回调地狱。