先记住这个答案
因为规范就这么定的。WHATWG 的 Encoding 标准里,TextEncoder 只支持 UTF-8,构造函数没有编码参数,它的 encoding 属性永远返回 'utf-8'。早期规范曾允许传编码名,后来删掉了,原因是 Web 平台希望新产生的数据都用 UTF-8,不再鼓励生成老编码。所以 new TextEncoder('gbk') 不会报错,但参数被直接忽略,输出还是 UTF-8。要生成 GBK 字节只能靠第三方库或者服务端。
TextEncoder只输出 UTF-8,传参也改不了- ASCII 占 1 字节,常用汉字 3 字节,emoji 4 字节
- 字节数要看
byteLength,不能看字符串length encodeInto写进已有数组,返回read和written
一个字符会变成几个字节
UTF-8 是变长编码:ASCII 字符 1 个字节,常用汉字 3 个字节,emoji 4 个字节。这决定了字符串长度和字节数经常对不上。
encode 的结果JavaScriptconst encoder = new TextEncoder()
console.log(encoder.encoding) // 'utf-8',固定值
const bytes = encoder.encode('A你😀')
console.log([...bytes])
// [65, 228, 189, 160, 240, 159, 152, 128]
// A 占 1 个,你 占 3 个,😀 占 4 个
console.log('A你😀'.length) // 4,😀 在 UTF-16 里是 2 个单元
console.log(bytes.byteLength) // 8,真正的字节数同一个字符串,length 是 4,字节数是 8。length 数的是 UTF-16 单元(JavaScript 字符串内部的存储单位),和 UTF-8 字节没有关系。
这在实际开发里很常见:后端数据库字段限制 255 字节,前端按 length 限制 255 个字符,用户输入全是中文时实际有 765 字节,入库直接失败。算字节数的正确写法是 new TextEncoder().encode(text).byteLength。
encode 和 encodeInto 怎么选
encode 每次返回一个新的 Uint8Array,简单直接,大多数场景用它就行。encodeInto 是往一个你已经准备好的数组里写,不分配新内存,适合高频循环或者往 WebAssembly 的内存里写字符串。
- 空间不够时
encodeInto怎么处理 encodeInto(text, target)返回一个对象{ read, written }。read是读了多少个 UTF-16 单元,written是写了多少字节。比如目标数组只有 4 个字节,写'A你好':A 占 1 字节,「你」占 3 字节,刚好写满;「好」放不下,它不会写半个进去,而是停下。返回{ read: 2, written: 4 },调用方看到read小于字符串长度,就知道还有剩余,从第 2 个单元继续写。 这里之所以要两个数字,是因为输入按 UTF-16 单元计、输出按字节计,两者的比例不固定,只给一个数没法知道下次从哪继续。
容易答错的地方
- 以为传参能指定编码
new TextEncoder('gbk')不报错,看起来像生效了,实际输出还是 UTF-8。要是拿这个结果去对接只认 GBK 的老系统,中文全是乱码。- 用
length给encodeInto预分配空间 new Uint8Array(text.length)对中文字符串是不够的。安全的上限是text.length * 3:一个 UTF-16 单元最多编码成 3 个字节,emoji 虽然是 4 字节,但它本身占 2 个单元,也在这个上限内。- 以为
TextEncoder会保留所有字符串原样 - JavaScript 字符串里可能有孤立的代理项,比如把 emoji 从中间截断留下的半个。它不是合法的 Unicode 字符,
TextEncoder会把它换成替换字符U+FFFD,编码成EF BF BD三个字节。原来的半个 emoji 就这样丢了,而且不报错。
面试官还会怎么问?
怎么把 UTF-8 字节转回字符串?
用 new TextDecoder().decode(bytes)。解码器默认也是 UTF-8,和编码器正好是一对。如果字节是分块到达的,要传 { stream: true } 防止一个字被切成两半。
以前没有 TextEncoder 时怎么算字节数?
老写法是 unescape(encodeURIComponent(str)).length,利用 encodeURIComponent 按 UTF-8 转成百分号编码,再还原成每个字节一个字符。unescape 已经被标记为废弃,现在直接用 TextEncoder,或者用 new Blob([str]).size。
为什么 TextDecoder 能解 GBK,TextEncoder 却不能编 GBK?
解码要面对已经存在的大量老数据,老网页、老文件都是 GBK、Big5,浏览器必须能读。编码是产生新数据,规范的立场是新数据一律用 UTF-8,所以只保留了 UTF-8。这是有意的设计,不是遗漏。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。