Table of Contents
关于 unicode 编码和 utf8 的对应关系可以参考知乎的一篇文章:《UTF-8 到底是什么意思?unicode编码简介》
根据上面的图,可以编写出 unicode 和 uint8 数组的相互转换函数。
编码
// 编码 function encode(str) { const arr = [...str] const buffer = new Uint8Array(arr.length * 4) let index = 0 for (let i = 0; i < arr.length; i++) { const codePoint = arr[i].codePointAt(0) // 四字节字符 if (codePoint >= 0x10000) { buffer[index++] = (codePoint >> 18) & 0x7 | 0xf0 buffer[index++] = (codePoint >> 12) & 0x3f | 0x80 buffer[index++] = (codePoint >> 6) & 0x3f | 0x80 buffer[index++] = codePoint & 0x3f | 0x80 } else if (codePoint >= 0x800) { // 三字节字符 buffer[index++] = (codePoint >> 12) & 0xf | 0xe0 buffer[index++] = (codePoint >> 6) & 0x3f | 0x80 buffer[index++] = codePoint & 0x3f | 0x80 } else if (codePoint >= 0x80) { // 两字节字符 buffer[index++] = (codePoint >> 6) & 0x1f | 0xc0 buffer[index++] = codePoint & 0x3f | 0x80 } else { // 单字节字符 buffer[index++] = codePoint } } return buffer.slice(0, index) }
解释:
- 初始 buffer 长度设为字符串长度的 4 倍。这里假设每一个字符都需要 4 个字节。用展开运算符将字符串转为数组是为了正确处理四字节字符。当然也可以直接根据码点判断要不要 i 额外 +1,这样可以节省解构的开销。
- 上图二进制中的 x 是数据位,其余有默认值的是控制位。1 ~ 4 字节字符数据位分别是 7、11、16、21。编码要做的就是把字符码点的二进制位填进 x 占据的位置。
- emoji 表情:🤔 是四字节,码点是 129300,二进制表示是 1 1111 1001 0001 0100。填充到四字节模板就是 11110000 10011111 10100100 10010100。(从低位开始,没有就补 0)
- (codePoint >> 18) & 0x7 | 0xf0 。这句的作用是填充第一个字节的三个数据位。首先右移 18 位,保留高三位(按位与 0x7,十六进制 7 就是二进制 111)。然后按位或 0xf0(11110000——四字节模板的第一个字节)。
- 其余操作同理。
解码
// 解码 function decode(buffer) { let str = '' for (let i = 0; i < buffer.length; i++) { switch (buffer[i] >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case7: str += String.fromCodePoint(buffer[i]) break case 12: case 13: str += String.fromCodePoint( ((buffer[i] & 0x1f) << 6) + (buffer[i + 1] & 0x3f) ) i++ break case 14: str += String.fromCodePoint( ((buffer[i] & 0xf) << 12) + ((buffer[i + 1] & 0x3f) << 6) + (buffer[i + 2] & 0x3f) ) i += 2 break case 15: str += String.fromCodePoint( ((buffer[i] & 0x7) << 18) + ((buffer[i + 1] & 0x3f) << 12) + ((buffer[i + 2] & 0x3f) << 6) + (buffer[i + 3] & 0x3f) ) i += 3 break } } return str }
解释:
- 解码是编码的逆向操作。移位和按位与就不解释了。这里说明一下 switch case 的判断。
- 从上面的图可以看出来,根据第一个字节的前四位可以判断目标字符属于哪个范围(需要几个字节)。
- 一个字节的字符,前四位处于 0b0000 ~ 0b0111 之间,即 0 ~ 7。
- 两个字节的字符,前四位处于 0b1100 ~ 0b1101 之间,即 12 ~ 13。
- 三个字节的字符,前四位是 0b1110,即 14。
- 四个字节的字符,前四位是 0b1111,即 15。
其实现在浏览器和 nodejs 都提供了字符串与 uint8array 的转换。
const decoder = new TextDecoder() const encoder = new TextEncoder() console.log(decoder.decode(encoder.encode('🤔'))) // 🤔
自己写编解码主要是为了了解一下原理,以及某些场景下修改得更高效。比如编解码直接使用调用者传递的 buffer 参数,避免函数内部新建 buffer 的开销,降低内存使用和时间。