【深入 Base64】Javascript 的字符集及编码(二)

上一节了解了 Base64 的编码原理和过程,这一节了解一下 Javascript 的字符集和字符编码,为后面的编解码处理做好准备。

 ASCII码

ASCII 码是美国针对英语字符制定的一套字符编码(或者叫字符集)。ASCII 是单字节编码,用 8 位二进制表示一个英文字符,其最高位总为 0,字符范围为 00000000 ~ 0fffffff(128个)。比如空格 "SPACE" 是32(二进制00100000),大写的字母 A 是 65(二进制01000001)。

非 ASCII 码

因为 ASCII 码表示的字符数量太少了,所以各个国家会针对 ASCII 码进行重新编码,利用其闲置的最高位来进行扩展,最多能表示 256 个字符;但到了东南亚国家,256 个字符仍然不够用,这时会用两个字节表示一个字符,比如 GB2312,其能表示的字符达到了 256 * 256 的数量。

UNICODE码

因为上面出现的不同编码,会导致一个非 ASCII 码,如果不知道编码规则的情况下,在不同的地区会表示成不同的字符,比如我们经常出现打开文件乱码。为了解决这个问题,我们需要制定统一标准,让全世界所有的字符都有唯一的编码,这就是Unicode,它是一种所有符号的编码。

但是,unicode 只是一种字符集,并没有说明编码格式,也就是说一个字符所对应的 unicode 十六进制值,我可以以任何形式进行存储,这也是造成 unicode 码一开始没有大规模流行的原因。

UTF-8

互联网的普及,强烈需要一种统一的编码方式。UTF-8 就是现在使用最广的一种 Unicode 的编码实现。其他编码还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。

UTF-8 是一种变长的编码方式。它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8 的编码规则很简单,只有二条:

  1. 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
  2. 对于 n 字节的符号(n>1),第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 unicode 码。

Unicode 字符范围(十六进制)

Utf-8 编码(二进制)

0000 0000 ~ 0000 007F 0xxxxxxx
0000 0080 ~ 0000 07FF 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8 编码非常简单。如果一个字节的第一位是 0,则这个字节单独就是一个字符;如果第一位是 1,则连续有多少个 1,就表示当前字符占用多少个字节。

比如“风”,其 unicode 码为 98CE,对应的二进制码为 1001 1000 1100 1110,按照上面规则转换后的二进制码为 11101001 10100011 10001110,对应的十六进制码为 E9A38E,其正对应着“风”的 utf-8 码。

UCS-2

JavaScript 语言采用 Unicode 字符集,但是只支持一种编码方法。这种编码既不是 UTF-16,也不是 UTF-8,更不是 UTF-32,而是 UCS-2。

什么是 UCS-2?这就需要讲一点历史。

在很久以前,曾经有两个团队同时想搞统一字符集,一个是 1988 年成立的 Unicode 团队,另一个是 1989 年成立的 UCS 团队。等到他们发现了对方的存在时,他们达成一致:世界上不需要两套统一字符集。于是在 1991 年 10 月,两个团队决定合并字符集,从今以后只发布一套字符集,就是 Unicode,并且修订此前发布的字符集,UCS 的码点将与 Unicode 完全一致。

但是 UCS 的开发进度快于 Unicode,1990 年就公布了第一套编码方法 UCS-2,而 Unicode 在 1996 年 7 月才公布第一套编码 UTF-16。而 Javascript 是 1995 年才出现的,所以,不是它不想采用别的编码方案,而是只有 UCS-2 这一套编码方案。

Javascript 实现 Unicode 转 UTF-8

因为 Javascript 的字符集是 unicode,而且 Javascript 中提供了读取字符 unicode 值的方法 charCodeAt,所以我们可以一个一个字符地读取 unicode 值,然后按照上面的规则进行转换。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Javascipt 实现 Utf-8 编解码</title>
</head>
<body>
<div>
    <span>输入要编码的字符串</span>
    <input type="text" id="input">
</div>
<div>
    <span>utf-8 编码:</span>
    <span id="utf-8"></span>
</div>
<div>
    <span>utf-8 解码:</span>
    <span id="unicode"></span>
</div>
<script>
    function unicode2utf8(str) {
        var charCode,
            outStr = "",
            len = str.length;
        for (var i = 0; i < len; i++) {
            charCode = str.charCodeAt(i);

            if (charCode <= 0x7F) { // 单字节
                outStr += str.charAt(i);
            } else if (charCode <= 0x7FF) { // 双字节
                outStr += String.fromCharCode(0xC0 | ((charCode >> 6) & 0x1F));
                outStr += String.fromCharCode(0x80 | ((charCode >> 0) & 0x3F));
            } else if (charCode <= 0xFFFF) { // 三字节
                outStr += String.fromCharCode(0xE0 | ((charCode >> 12) & 0xF));
                outStr += String.fromCharCode(0x80 | ((charCode >> 6) & 0x3F));
                outStr += String.fromCharCode(0x80 | ((charCode >> 0) & 0x3F));
            } else { // 四字节
                outStr += String.fromCharCode(0xF0 | ((charCode >> 18) & 0x7));
                outStr += String.fromCharCode(0x80 | ((charCode >> 12) & 0x3F));
                outStr += String.fromCharCode(0x80 | ((charCode >> 6) & 0x3F));
                outStr += String.fromCharCode(0x80 | ((charCode >> 0) & 0x3F));
            }
        }
        return outStr;
    }

    function utf82unicode(str) {
        var len = str.length,
            outStr = "",
            charCode,
            h1, h2, h3, h4;

        for (var i = 0; i < len; i++) {
            charCode = str.charCodeAt(i);

            if (charCode < 0xC0) { // 单字节
                outStr += str.charAt(i);
            } else if (charCode < 0xE0) { // 双字节
                h1 = (charCode & 0x1F) << 6;
                h2 = (str.charCodeAt(++i) & 0x3F) << 0;
                outStr += String.fromCharCode(h1 | h2);
            } else if (charCode < 0xF0) { // 三字节
                h1 = (charCode & 0xF) << 12;
                h2 = (str.charCodeAt(++i) & 0x3F) << 6;
                h3 = (str.charCodeAt(++i) & 0x3F) << 0;
                outStr += String.fromCharCode(h1 | h2 | h3);
            } else { // 四字节
                h1 = (charCode & 0x7) << 18;
                h2 = (str.charCodeAt(++i) & 0x3F) << 12;
                h3 = (str.charCodeAt(++i) & 0x3F) << 6;
                h4 = (str.charCodeAt(++i) & 0x3F) << 0;
                outStr += String.fromCharCode(h1 | h2 | h3);
            }
        }
        return outStr;
    }

    document.querySelector("#input").addEventListener("input", function () {
        var utf8 = unicode2utf8(this.value);
        document.querySelector("#utf-8").innerText = utf8.split('').map(s => s.codePointAt().toString(16)).join(' ');
        document.querySelector("#unicode").innerText = utf82unicode(utf8);
    })
</script>
</body>
</html>