Buffer 基础:用"字节"的视角看数据
本节目标
- 搞懂为什么 JS 字符串不够用,必须引入 Buffer 处理二进制
- 掌握 Buffer 的四种创建方式、读写方法、以及 UTF-8 / hex / base64 互转
- 理解"字节序(大小端)"是什么,为什么网络传输要统一约定
JavaScript 的字符串是"字符序列",底层按 UTF-16 存。可一旦你和文件、网络、图片、压缩包打交道,面对的就是一串原始字节(byte,8 个比特,范围 0–255)。字符串和字节不是一回事:一个汉字在 UTF-8 里占 3 个字节,但在字符串里只是一个"字符"。所以处理二进制,Node 提供了 Buffer——一块连续的内存,按"字节"来读写。
四种最常用的创建方式:
// 运行环境:Node.js 14+(Buffer 是 Node 内置全局对象,无需安装任何包)
// 保存为 buf-l1.js,执行:node buf-l1.js
// 1) 从字符串创建(默认按 UTF-8 编码成字节)
const buf1 = Buffer.from('你好Hi')
console.log('字节长度 :', buf1.length) // "你""好"各 3 字节,"H""i"各 1 字节 => 8
console.log('十六进制 :', buf1.toString('hex')) // e4bda0e5a5bd4869
console.log('Base64 :', buf1.toString('base64'))
// 2) 从字节数组精确构造(每个数字 0-255 就是一个字节)
const buf2 = Buffer.from([0x48, 0x69, 0x21]) // 0x48=H 0x69=i 0x21=!
console.log('数组构造 :', buf2.toString()) // Hi!
// 3) 预分配固定大小再写入(网络收包、拼报文时最常用,避免反复分配内存)
const buf3 = Buffer.alloc(4)
buf3.writeUInt8(10, 0) // 第 0 字节写入 10
buf3.write('AB', 1, 'utf8') // 从第 1 字节起写入 "AB"
console.log('预分配 :', buf3, buf3.toString()) // <Buffer 0a 41 42 00> -> " AB"
// 4) 多字节整数与字节序(大小端)
const n = 0x12345678
const be = Buffer.alloc(4); be.writeUInt32BE(n, 0) // 大端:高位在前 -> 12 34 56 78
const le = Buffer.alloc(4); le.writeUInt32LE(n, 0) // 小端:低位在前 -> 78 56 34 12
console.log('大端 hex :', be.toString('hex')) // 12345678
console.log('小端 hex :', le.toString('hex')) // 78563412
console.log('读回整数 :', le.readUInt32LE(0)) // 305419896 = 0x12345678
// 5) 切片 subarray(注意:与原 Buffer 共享同一块内存!)
const base = Buffer.from('abcdefgh')
const sub = base.subarray(2, 5) // 取第 2~4 字节 -> "cde"
sub[0] = 0x58 // 0x58 = 'X'
console.log('原 buffer 被改:', base.toString()) // abXdefgh为什么要有字节序? 一个 32 位整数占 4 个字节,但这 4 个字节"谁排前面"有两种约定:BE(Big-Endian,大端,高位在前,网络协议通用)和 LE(Little-Endian,小端,x86 CPU 内部用)。writeUInt32BE/LE 与 readUInt32BE/LE 就是用来显式指定,避免"我发的数字你读成别的数字"。
名词解释
- 缓冲区(Buffer):一段连续的内存,按字节(0–255)读写,专门承载二进制数据。你可以把它想成"一排编号的小格子,每格放 0–255 的一个数"。
- 字节(Byte):8 个比特(bit),计算机存储的最小可寻址单位,范围是 0–255。两个十六进制字符正好表示一个字节(如
0x4a)。 - 字符编码(如 UTF-8):规定"字符 ↔ 字节"如何转换的规则。同一个字符(如"你")按不同编码会变成不同字节序列;
Buffer.toString('utf8')就是按 UTF-8 把字节翻译回字符串。 - 字节序 / 大小端(Endianness):多字节数据在内存里"高位字节放前面还是后面"的约定。网络协议几乎都用大端(BE),CPU 内部多用小端;不统一就会导致"发 1 收到 16777216"。
- subarray(切片):从原 Buffer 中"切出"一段视图。关键陷阱:切片与原 Buffer 共享底层内存,改切片会影响原数据;想要独立副本用
Buffer.from(sub)。
课后练习
Buffer.from('你好')的length是 2 还是 6?为什么?- 答案:是 6。
'你好'是两个字符,但 UTF-8 下一个汉字占 3 字节,所以共 6 个字节。这正说明"字符数 ≠ 字节数",处理二进制必须看字节。
- 答案:是 6。
- 下面代码输出什么?为什么?
const a = Buffer.from('abc') const b = a.subarray(0, 2) b[0] = 0x5a console.log(a.toString())- 答案:输出
Zbc。因为subarray与原a共享内存,把b[0]改成0x5a('Z') 同时改动了a的第 0 字节。若要互不干扰,应用Buffer.from(a.subarray(0,2))复制一份。
- 答案:输出
- 网络传输一个 32 位整数,为什么必须用
writeUInt32BE而不是依赖默认?- 答案:不同机器默认字节序不同(x86 是小端),直接写会在发送方和接收方 CPU 不一致时解读错误。网络协议统一规定大端(BE),用
BE显式写出,双方才一致。
- 答案:不同机器默认字节序不同(x86 是小端),直接写会在发送方和接收方 CPU 不一致时解读错误。网络协议统一规定大端(BE),用
总结
Buffer 是 Node 处理真实世界数据的"地基"。我特别想强调一个认知转变:在写业务代码时你习惯"字符串 = 文本",但一旦涉及文件、网络、压缩、加密,你面对的全部是字节流——HTTP body、图片像素、音频采样、数据库记录,本质上都是一连串 0–255 的数字。字符串只是其中"恰好能被 UTF-8 翻译"的一小部分。理解 Buffer 之后,你会自然地用"字节"的视角重新审视一切 I/O:读文件不是读"文字"而是读"字节块",发请求不是发"字符串"而是发"字节序列"。这也正是为什么 Buffer.from 几乎出现在今天之后每一节课里——它和 Stream 一起,构成了 Node 底层能力的两根支柱。把"字符数 ≠ 字节数""大小端决定数字读出来对不对""切片会共享内存"这三件事刻进肌肉记忆,你就能避开 90% 的二进制坑。