TextDecoder 的 ignoreBOM:设为 true,为什么开头反而多出一个不可见字符

前天 3阅读

读取带 UTF-8 BOM 的短文本时,开发者把 ignoreBOM 设为 true,期待忽略文件开头的标记,字符串却多了一个 U+FEFF。这个名字容易被理解反了:true 会忽略BOM的特殊处理,使对应字符进入结果;默认 false 才会在流开端识别并省略它。

下面固定使用两个带BOM的小输入,各自正文为 A 与 B。保存为 demo.mjs,运行 node demo.mjs。程序用十六进制码点打印结果,不靠不可见字符的控制台外观猜测,也不读取任何真实文件。

TextDecoder 的 ignoreBOM:设为 true,为什么开头反而多出一个不可见字符

AI模型生成概念示意:同一前导标记可以被识别后省略,也可以作为内容保留;是解码选项的抽象图,不是实测界面。

import assert from 'node:assert/strict';

const bom = [0xef, 0xbb, 0xbf];
const first = Uint8Array.from([...bom, 0x41]);
const second = Uint8Array.from([...bom, 0x42]);
const points = text => [...text].map(char =>
  char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')
).join(' ');
const ordinary = new TextDecoder('utf-8').decode(first);
const kept = new TextDecoder('utf-8', { ignoreBOM: true }).decode(first);
assert.equal(ordinary, 'A');
assert.equal(kept, '\uFEFFA');
console.log('default: ' + points(ordinary));
console.log('ignoreBOM=true: ' + points(kept));

const stream = new TextDecoder('utf-8');
const combined = stream.decode(first, { stream: true })
  + stream.decode(second, { stream: true }) + stream.decode();
assert.equal(combined, 'A\uFEFFB');
console.log('one stream: ' + points(combined));

const independent = new TextDecoder('utf-8');
const separate = independent.decode(first) + independent.decode(second);
assert.equal(separate, 'AB');
console.log('two complete inputs: ' + points(separate));

默认开关处理的是开端标记

default: 0041 表明结果只有 A。ignoreBOM=true: FEFF 0041 则说明 U+FEFF 被保留下来。输入字节完全一样,变化只来自选项。判断有没有BOM残留,应检查首字符的码点或直接比较字符串,不能仅凭屏幕上两行看起来相同。

这里的三个字节 EF BB BF 对应 UTF-8 的BOM。示例已经明确指定 utf-8,讨论的是这个解码器如何输出开端字符,并不是靠BOM任意猜测文件编码。该选项也不等于把文本中所有 U+FEFF 都删除;中间出现的同一码点需要按正文位置理解。

同一条流只在开始处作这次判断

one stream: 0041 FEFF 0042 来自两次 stream: true 和最后一次收尾调用。第二块虽然也从BOM字节开始,但它并不是这条流的开端,所以对应 U+FEFF 留在 A 与 B 之间。网络分块或者读取块的边界,不会自动成为一个新文档的起点。

two complete inputs: 0041 0042 则来自两次普通 decode。每次都完成一份输入,下一次会重新初始化解码状态与开端判断,因此两个BOM都被省略。复用同一个 TextDecoder 对象,并不代表所有调用永远属于同一条流;stream 参数决定是否继续保留这轮状态。

先确认字节属于一个文档还是两个文档

如果数据是一个文件分成若干块,应维持同一解码流并在结束时收尾。如果确实是两份独立文件,就应分别完成解码。为了消除中间那个不可见字符而随意改成逐块独立 decode,会改变输入的文档边界,还可能破坏跨块字符,不能当作通用修补。

导入器可以用这四行结果设计测试:默认省略开端BOM、显式保留BOM、单流中间保留、独立输入分别省略。需要剔除正文中某个特殊字符时,那是后续内容清理规则,应说明允许的位置与理由,避免解码层替业务悄悄删除真实内容。

资料核对日期:2026年10月2日。完整代码在 Node.js v24.19.0 实跑,字符串与码点断言全部通过。

官方参考

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。