☝🏻前端er,了解工具函数底层,UTF-8和Unicode的转换

145 阅读4分钟

Encoding API两个API,核心代码底层实现(TextDecoder、TextEncoder)。

no.1 从Unicode字符串 转成 UTF-8字节列

传参: 传字符串;

里面做法:循环 字符串中 每个字符;根据字符的Unicode码点值,按照UTF-8编码规则将其转换为1-4个字节。最后,转换后的字节,推到一个数组中 返 Uint8Array类型数组出去。

static stringToUtf8ByteArray = function(str: string) {
    var out = [], p = 0; // 准备一个空数组和指针,就像准备一张白纸开始作画
    
    for (var i = 0; i < str.length; i++) {
        var c = str.charCodeAt(i); // 获取当前字符的Unicode码点
        
        // ASCII字符的简单世界(U+0000到U+007F)
        if (c < 128) {
            out[p++] = c; // 直接存储,无需任何转换
        } 
        
        // 两字节编码的领域(U+0080到U+07FF)
        else if (c < 2048) {
            // 第一个字节:110xxxxx(前5位)
            out[p++] = (c >> 6) | 192; // 192是0b11000000
            // 第二个字节:10xxxxxx(后6位)
            out[p++] = (c & 63) | 128;  // 128是0b10000000
        } 
        
        // 代理对处理的奇妙世界(U+10000到U+10FFFF)
        else if ((c & 0xFC00) == 0xD800 && i + 1 < str.length &&
                (str.charCodeAt(i + 1) & 0xFC00) == 0xDC00) {
            // 组合高低代理对,形成完整的码点
            c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF);
            
            // 四字节编码:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
            out[p++] = (c >> 18) | 240;       // 240是0b11110000
            out[p++] = ((c >> 12) & 63) | 128; // 取18-13位
            out[p++] = ((c >> 6) & 63) | 128;  // 取12-7位
            out[p++] = (c & 63) | 128;         // 取6-1位
        } 
        
        // 三字节编码的常规情况(U+0800到U+FFFF,不包括代理区)
        else {
            // 三字节编码:1110xxxx 10xxxxxx 10xxxxxx
            out[p++] = (c >> 12) | 224;       // 224是0b11100000
            out[p++] = ((c >> 6) & 63) | 128; // 取12-7位
            out[p++] = (c & 63) | 128;         // 取6-1位
        }
    }
    return Uint8Array.from(out); // 将普通数组转换为Uint8Array
};

TextEncoderAPI:

const encoder = new TextEncoder();
const view = encoder.encode("€");
console.log(view); // Uint8Array(3) [226, 130, 172]

image.png

no.2 将UTF-8编码的字节数组转换回JavaScript字符串

传参: UTF-8编码的字节流(Uint8Array类型);

里面做法

  • 根据UTF-8的编码规则 识别 每个字符占用的 字节数

  • 将1-4个字节的UTF-8序列 重新组合成 Unicode码点

  • 处理代理对特殊情况(将4字节UTF-8转换为UTF-16代理对)

  • 最终拼接成完整的JavaScript字符串

static utf8ByteArrayToString(bytes: Uint8Array): string {
    var out = [], pos = 0, c = 0; // 准备输出数组和两个指针
    
    while (pos < bytes.length) {
        var c1 = bytes[pos++]; // 读取第一个字节
        
        // 单字节编码的简单情况(0xxxxxxx)
        if (c1 < 128) {
            out[c++] = String.fromCharCode(c1); // 直接转换
        } 
        
        // 两字节编码的情况(110xxxxx 10xxxxxx)
        else if (c1 > 191 && c1 < 224) {
            var c2 = bytes[pos++];
            // 组合两个字节:((c1 & 00011111) << 6) | (c2 & 00111111)
            out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63);
        } 
        
        // 四字节编码的代理对情况(11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
        else if (c1 > 239 && c1 < 365) {
            var c2 = bytes[pos++];
            var c3 = bytes[pos++];
            var c4 = bytes[pos++];
            
            // 计算完整的Unicode码点
            var u = ((c1 & 7) << 18 | (c2 & 63) << 12 | 
                    (c3 & 63) << 6 | c4 & 63) - 0x10000;
            
            // 转换为UTF-16代理对
            out[c++] = String.fromCharCode(0xD800 + (u >> 10)); // 高代理
            out[c++] = String.fromCharCode(0xDC00 + (u & 1023)); // 低代理
        } 
        
        // 三字节编码的常规情况(1110xxxx 10xxxxxx 10xxxxxx)
        else {
            var c2 = bytes[pos++];
            var c3 = bytes[pos++];
            // 组合三个字节:((c1 & 00001111) << 12) | ((c2 & 00111111) << 6) | (c3 & 00111111)
            out[c++] = String.fromCharCode(
                (c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
        }
    }
    return out.join(''); // 将所有字符拼接成完整的字符串
};

更简单API:

// 使用TextDecoder API
function utf8ByteArrayToString(bytes) {
    return new TextDecoder('utf-8').decode(bytes);
}

哪里可以用

第一☝🏻,前后端传数据流,传命令行,原原本本传过去,又不想穿txt等文本流文件过去,就用这个。

第二,正好,我也可以记录一下底层咋实现的。

第三,有些环境没有这些原生的简单的api,叫我们去实现,比如说,一些后端环境里面没得,要自己手写。

第四,要是有什么奇怪的需求,混合着来的那种,也好自己在原生方法里面魔改。