先记住这个答案
BOM 是放在文本最开头的一个特殊字符 U+FEFF,用来标记编码。UTF-8 编码下它是三个字节 EF BB BF。TextDecoder 默认会在流的开头检查有没有和当前编码匹配的 BOM,有的话就当作标记跳过,结果字符串里没有它。如果传了 ignoreBOM: true,意思是「忽略 BOM 的特殊含义」,解码器不再把它当标记,于是它作为普通字符 \uFEFF 留在了结果开头。很多人以为 ignoreBOM 是「忽略掉、删掉 BOM」,正好理解反了。
- UTF-8 的 BOM 是三个字节
EF BB BF - 默认会去掉开头的 BOM
ignoreBOM: true反而保留 BOM- 分块解码要复用同一个 decoder
默认和 ignoreBOM: true 的对比
在字母 A 前面加上 UTF-8 的 BOM,用两种选项分别解码。
const bytes = new Uint8Array([0xef, 0xbb, 0xbf, 0x41]) // BOM + 'A'
const normal = new TextDecoder('utf-8').decode(bytes)
const kept = new TextDecoder('utf-8', { ignoreBOM: true }).decode(bytes)
console.log(normal, normal.length) // A 1,BOM 被去掉了
console.log(kept.length) // 2,BOM 还在
console.log(kept.codePointAt(0).toString(16)) // feff默认解码得到的字符串只有一个字符 A。开了 ignoreBOM 之后长度是 2,第一个字符是 U+FEFF。在控制台里打印 kept,看起来和 A 一模一样,因为 BOM 是不可见字符,这也是它难排查的原因。
「ignore」在这里的意思是「不理会它的签名作用」,不是「把它扔掉」。真正把它扔掉的,恰恰是默认行为。
BOM 在实际项目里怎么坑人
UTF-8 其实不需要 BOM,因为它按单字节读,没有字节序的问题。BOM 原本是为 UTF-16 设计的,用来区分大端和小端。但 Windows 上的一些工具保存 UTF-8 文件时会带上它,最典型的就是 Excel。
- 带 BOM 的 JSON 解析失败
- 有时候
JSON.parse读一个配置文件,报Unexpected token,打开文件看又完全正常,很可能就是开头有 BOM。fetch的res.json()和res.text()内部会去掉 UTF-8 BOM,不会有问题。但用 Node 的fs.readFileSync(path, 'utf8')读文件时,BOM 会保留在字符串开头,JSON.parse碰到\uFEFF就报错。这时要么检查str.charCodeAt(0) === 0xfeff后去掉第一个字符,要么用TextDecoder默认选项解码。 - 导出 CSV 时反而要加 BOM
- 反过来,前端导出 CSV 给用户用 Excel 打开时,如果不加 BOM,Excel 会按系统默认编码去读,中文全变乱码。常见做法是在内容前拼一个
'\uFEFF'再生成 Blob。这就是 BOM 现在最主要的用途:给不支持编码声明的老软件一个提示。 - 分块解码要复用同一个 decoder
- 读大文件或流式响应时,数据是一块一块来的,要用
decoder.decode(chunk, { stream: true })并且始终用同一个TextDecoder实例。它会记住自己是否已经过了流的开头,只在第一块检查 BOM,也能正确处理被切断在两块之间的多字节字符。每块都 new 一个新的 decoder,被切断的中文字符会变成乱码。
容易答错的地方
- 以为
ignoreBOM: true会删掉 BOM - 正好相反。默认
false才会跳过 BOM,设成true是关闭这层特殊处理,BOM 会留在结果里。需要 BOM 原样保留的场景很少,比如要把文本原封不动写回文件。 - 对整个字符串全局替换
\uFEFF - BOM 只在开头才有编码标记的含义。正文里出现的
U+FEFF历史上用作零宽不换行空格,可能是有意放的。要去 BOM,只处理第一个字符,别用replace(/\uFEFF/g, '')全删。
面试官还会怎么问?
UTF-8 文件必须带 BOM 吗?
不需要,而且 Unicode 标准并不推荐 UTF-8 带 BOM。它只是为了兼容某些软件,比如让 Excel 正确识别编码。网页和接口应该用 Content-Type 头里的 charset 声明编码,而不是靠 BOM。
TextDecoder 会根据 BOM 自动切换编码吗?
不会。TextDecoder 在构造时就定死了编码,它只会检查和这个编码匹配的 BOM。比如用 utf-8 解码器去读一个 UTF-16LE 的文件,开头的 FF FE 不会被当成 BOM,结果是乱码。根据 BOM 嗅探编码是 HTML 解析器做的事,不是 TextDecoder。
BOM 会影响字符串比较吗?
会。带 BOM 的 '\uFEFFname' 和 'name' 用 === 比较是 false,长度也差 1。读 CSV 时表头第一列的列名常常因此匹配不上,调试时可以打印 str.charCodeAt(0) 看看是不是 65279。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。