Buffer 练习答案(一):1~16 题

适用环境:Node.js 22+、TypeScript。本册关注 UTF-8 字节、Buffer 的创建方式,以及共享内存视图。示例可保存为 .ts 文件并用 ts-node 运行。

Buffer 是 Node.js 对二进制数据的常用表示;它继承自 Uint8Arraybuffer.length 永远是字节数,不是字符数。

1. 创建 hello Buffer

const buffer = Buffer.from("hello", "utf8");

console.log(buffer.toString("hex")); // 68656c6c6f
console.log(buffer.length); // 5
console.log(buffer.toString("utf8")); // hello

UTF-8 中 ASCII 字符各占一个字节,因此长度是 5。

2. 中文 的字节长度

const text = "中文";
const buffer = Buffer.from(text, "utf8");

console.log(text.length); // 2:JavaScript UTF-16 code unit 数
console.log(buffer.length); // 6:UTF-8 字节数
console.log(buffer.toString("hex")); // e4b8ade69687

常用汉字在 UTF-8 中通常占 3 字节。注意:string.length 也不总等于用户看到的字符数,例如 emoji 可能占两个 UTF-16 code unit;需要按用户感知字符分割时用 Intl.Segmenter,不要用 Buffer 切字节。

3. 用字节数组创建 123

const decimal = Buffer.from([49, 50, 51]);
const hexadecimal = Buffer.from([0x31, 0x32, 0x33]);

console.log(decimal.toString()); // 123
console.log(hexadecimal.toString()); // 123

490x31 是同一个数值;它是字符 "1" 的 UTF-8/ASCII 字节,不是数字 1。

4. "0"[0]"\\0"

const a = Buffer.from("0");
const b = Buffer.from([0]);
const c = Buffer.from("\0");

for (const item of [a, b, c]) {
  console.log({ hex: item.toString("hex"), length: item.length, text: item.toString("utf8") });
}
// a: 30, 1, "0"
// b: 00, 1, "\u0000"
// c: 00, 1, "\u0000"

"0" 是可见字符,字节为 0x30[0]"\0" 都是 NUL 空字节 0x00。终端可能看不见 NUL,但它仍在数据中。

5. 获取 UTF-8 字节长度

function utf8ByteLength(text: string): number {
  return Buffer.byteLength(text, "utf8");
}

console.log(utf8ByteLength("hello中文")); // 11

这比 text.length 更适合 HTTP Content-Length、数据库字节限制和上传内容大小校验。

6. 在 UTF-8 字符中间按字节截断

const source = Buffer.from("hello中文", "utf8");
const first5 = source.subarray(0, 5);
const first6 = source.subarray(0, 6);

console.log(first5.toString()); // hello
console.log(first6.toString()); // hello\uFFFD(终端会显示替换字符)
console.log(first6.toString("hex")); // 68656c6c6fe4

第 6 个字节是 的首字节 e4,不是完整 UTF-8 序列。toString("utf8") 会用 Unicode 替换字符(U+FFFD)表示非法或不完整序列。文本截断应按字符边界处理;流式解码应使用 StringDecoderTextDecoder 的流式能力。

7. 不完整中文序列

const full = Buffer.from("中");
console.log(full.toString("hex")); // e4b8ad

console.log(full.subarray(0, 1).toString()); // U+FFFD 替换字符
console.log(full.subarray(0, 2).toString()); // U+FFFD 替换字符
console.log(full.subarray(0, 3).toString()); // 中

不要依据 chunk.toString() 来拼接网络中的 UTF-8 文本:一个字符可能被拆到两个 TCP/HTTP chunk。正确方式是累积原始 Buffer,或使用 node:string_decoder

8. Base64 往返

const original = "Node.js";
const encoded = Buffer.from(original, "utf8").toString("base64");
const restored = Buffer.from(encoded, "base64").toString("utf8");

console.log(encoded); // Tm9kZS5qcw==
console.log(restored); // Node.js

Base64 是编码而不是加密;体积通常约增加 1/3,且不应把不可信 Base64 当作已验证的文件内容。

9. Hex 往返

const hex = Buffer.from("abc123", "utf8").toString("hex");
const restored = Buffer.from(hex, "hex");

console.log(hex); // 616263313233
console.log(restored.toString()); // abc123

Hex 每个字节用两个字符展示,体积约翻倍,适合调试、哈希和二进制日志展示,不适合大内容传输。

10. byteLength()Buffer.length

for (const text of ["hello", "中文", "😀"]) {
  console.log(text, Buffer.byteLength(text, "utf8"), Buffer.from(text, "utf8").length);
}

在相同 UTF-8 编码下,两者相等。前者只计算,不分配 Buffer;后者会创建实际字节数据。只需要长度时优先 Buffer.byteLength()

11. alloc() 与不同 fill()

for (const value of [0, "0", "A", ""] as const) {
  const buffer = Buffer.alloc(10);
  buffer.fill(value);
  console.log(JSON.stringify(value), buffer.toString("hex"), JSON.stringify(buffer.toString()));
}
// 0   => 00000000000000000000
// "0" => 30303030303030303030
// "A" => 41414141414141414141
// ""  => 00000000000000000000

Buffer.alloc(10) 已清零。字符串填充会重复其编码字节;空字符串不会写入任何字节,因此保留初始的零。多字节字符串也会按字节重复,可能在边界处切开字符,通常不要用它填充文本协议。

12. allocUnsafe() 的风险

const unsafe = Buffer.allocUnsafe(10);
console.log(unsafe.toString("hex")); // 每次运行都可能不同

const safe = Buffer.alloc(10);
console.log(safe.toString("hex")); // 00000000000000000000

allocUnsafe() 取得的是未初始化内存,可能包含同一进程中旧数据。它并非“有漏洞的 API”,但在写满前把内容发送、落盘或记录日志,可能泄露敏感信息。只有在性能确实关键、且能严格保证所有字节都会被覆盖时才使用;一般后端业务使用 Buffer.alloc()

13. 拼接两个 Buffer

const hello = Buffer.from("hello");
const world = Buffer.from("world");
const result = Buffer.concat([hello, world]);

console.log(result.toString()); // helloworld

Buffer.concat() 会分配新 Buffer 并复制数据。大量小块不断 concat 会产生重复复制;读取流时先收集 chunk、最后只 concat 一次,或直接使用 pipeline()

14. subarray() 截取 world

const source = Buffer.from("hello world");
const world = source.subarray(6);

console.log(world.toString()); // world

subarray() 是同一块底层内存的视图,创建成本低,但生命周期与修改关系必须明确。

15. 共享视图与独立副本

const original = Buffer.from("hello");
const view = original.subarray(0, 2);
const copied = Buffer.from(original);

view[0] = 0x48; // H
copied[1] = 0x41; // A

console.log(original.toString()); // Hello:view 修改影响原对象
console.log(copied.toString()); // hAllo:copied 是独立副本

subarray() 共享内存;Buffer.from(existingBuffer) 会拷贝。处理不可信输入、异步保存数据或将一段数据交给别的模块修改时,常需创建独立副本。

16. 拷贝到指定偏移量

const target = Buffer.alloc(8, 0x2e); // ........
const source = Buffer.from("abc");

source.copy(target, 2);
console.log(target.toString()); // ..abc...
console.log(target.toString("hex")); // 2e2e6162632e2e2e

copy(target, targetStart, sourceStart?, sourceEnd?) 返回实际复制的字节数。如果目标空间不够,它会静默只复制能放下的部分;构造协议时应先检查长度,避免得到截断包。