Buffer 基础:用"字节"的视角看数据

本节目标

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 就是用来显式指定,避免"我发的数字你读成别的数字"。

名词解释

课后练习

  1. Buffer.from('你好') 的 length 是 2 还是 6?为什么?
    • 答案:是 6。'你好' 是两个字符,但 UTF-8 下一个汉字占 3 字节,所以共 6 个字节。这正说明"字符数 ≠ 字节数",处理二进制必须看字节。
  2. 下面代码输出什么?为什么?
    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)) 复制一份。
  3. 网络传输一个 32 位整数,为什么必须用 writeUInt32BE 而不是依赖默认?
    • 答案:不同机器默认字节序不同(x86 是小端),直接写会在发送方和接收方 CPU 不一致时解读错误。网络协议统一规定大端(BE),用 BE 显式写出,双方才一致。

总结

Buffer 是 Node 处理真实世界数据的"地基"。我特别想强调一个认知转变:在写业务代码时你习惯"字符串 = 文本",但一旦涉及文件、网络、压缩、加密,你面对的全部是字节流——HTTP body、图片像素、音频采样、数据库记录,本质上都是一连串 0–255 的数字。字符串只是其中"恰好能被 UTF-8 翻译"的一小部分。理解 Buffer 之后,你会自然地用"字节"的视角重新审视一切 I/O:读文件不是读"文字"而是读"字节块",发请求不是发"字符串"而是发"字节序列"。这也正是为什么 Buffer.from 几乎出现在今天之后每一节课里——它和 Stream 一起,构成了 Node 底层能力的两根支柱。把"字符数 ≠ 字节数""大小端决定数字读出来对不对""切片会共享内存"这三件事刻进肌肉记忆,你就能避开 90% 的二进制坑。