ASCII Unicode UTF8 编码笔记

本文主要了解一下ASCII码、Unicode码和UTF-8码的来源和相互之间的关系, 顺便也理了一下中文编码GB2312, GBK, GB18030的关系。

# ASCII码

在上世纪60年代,美国制定了ASCII码,主要目的是为了用二进制编码的方式来表达英文字符,用一个8位的字节大小对应了128个字符,其中包括了可打印出来的96个字符和32个不可打印的控制字符, 规则是二进制中第1位固定为0, 后面7位用来编码, 刚好可以表示27 = 128个字符, 例如规定空格SPACE的编码为00100000, 十进制是32, 大写字母A的编码为01000001, 十进制是65, 附上ASCII码表

# GB2312, GBK, GB18030

# Unicode码

ASCII码虽然满足了美国的需求,但是对于其它语言而言128个字符是远远不够的, 比如法语中字母上方有注音, 这是ASCII码无法表示的, 又比如汉字有10万左右, 这也是超出了ASCII码的范围, 所以后来Unicode码出现了. Unicode码有着很大的容量, 现在的规模可以容纳100多万个符号, 每个符号的编码都不一样, 比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字。你可以使用在线的工具来转换成Unicode码.

# Unicode码编码方式

Unicode码只是定义了每个字符对应的二进制代码是什么, 但是并没有规定字符对应的二进制应该以什么样的形式存储, Unicode统一规定,每个符号用三个或四个字节表示. 比如汉字的Unicode码是十六进制数4E25, 转换成二进制就是100111000100101一共是15位, 至少占用2个字节的空间, 而其他的字符可能有更多的二进制位数, 而之前的ASCII码是固定为8位的, 如果采取将前面多余的位数全都置0的话, 那么在存储原来的ASCII码编码的文件时就会浪费大量的空间来存储无用的0信息, 这是不可接受的. 所以如何合理的用Unicode码来兼容原先的ASCII码信息就产生出了多种具体的实现方式.

# UTF-8实现Unicode

UTF-8是目前使用最多的Unicode编码实现方式, 除此之外也有 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示)实现方式, 不过基本不使用. UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。 按照如下两条规则来编码字符:

  1. 对于单(n = 1)字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

  2. 对于多(n > 1)字节的符号,第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。 下表总结了编码规则,字母x表示可用编码的位。

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

在解码的时候现查看二进制的第一位, 如果是0, 那么说明是单字节的字符, 直接将该字节按照Unicode码表转换成对应的字符即可, 如果第一位是1, 那么继续查看有几个连续的1, 有n个, 则说明连续的n个字节代表一个字符. 以汉字为例, 的Unicode码是4E25(二进制为100111000100101), 根据上表, 4E25处于0000 0800 - 0000 FFFF范围, 那么的编码格式就是1110xxxx 10xxxxxx 10xxxxxx, 也就是说的UTF-8编码方式就需要占用三个字节, 我们把的二进制按照顺序填到x的位置, 最后得到的结果就是11100100 10111000 10100101, 转成16进制就是E4B8A5, 这就是的UTF-8编码结果. 总得来说, 的Unicode码为4E25, UTF-8编码为E4B8A5, 这就好比你的身份证是123456, 在学校站队时老师按照一定的排队方式把你编排到了五组三排第二个, 这两者最后的结果是可以相互转换的, 你可借助在线工具验证.

# JavaScript中的Unicode与UTF-8

javascript程序是使用Unicode字符集编写的, 所以我们在JavaScript中经常使用的字符或者字符串实际上内部是采用Unicode编码的, 在有些情况下, 比如我们的服务器要求接受的二进制内容的编码必须是UTF-8, 那么我们在把JavaScript中的字符串发送到服务器之前就需要进行转码, 将Unicode字符串转为UTF-8字符串. 我们在前端有时候会看到的服务器返回的json数据中乱码实际上就是因为服务器发送数据的编码跟我们客户端接受数据的编码方式不一致导致的, 你可以试着将乱码字段拷贝到在线工具中进行转码, 比如选择将Unicode转为UTF-8, 然后你就能看到正确的信息.

除了数据交互之外, 浏览器的URI也是我们能够了解这种编码转换的地方, 因为URI中的querystring必须按照UTF8的编码进行传输, 但是JavaScript中是Unicode的, 如果没有中文信息还好, 因为英文字符在这两者之间的码值是保持一致的, JavaScript的字符串hello到了URI中也还是hello, 如果你不手动去转换也是ok的, 但是一旦涉及到中文(包括其它非英文字符), 比如汉字, 它的Unicode码值和UTF-8码值就差的很远, 如果你不进行手动转换, 直接将JavaScript中的字符丢到地址栏的URI中, 那么就会导致URI乱码, 你再想从URI中把之前放进去的取出来就会发现得到的根本不是汉字, 而是一串乱码.

# 在JavaScript中如何转换Unicode与UTF-8

//编码
encodeURIComponent(''); // => '%E4%B8%A5'

//解码
decodeURIComponent('%E4%B8%A5'); // => '严'

//encodeURI和encodeURIComponent对比
encodeURI('www.kricsleo.com?name="张三"'); // => "www.kricsleo.com?name=%22%E5%BC%A0%E4%B8%89%22"

encodeURIComponent('www.kricsleo.com?name="张三"') // => "www.kricsleo.com%3Fname%3D%22%E5%BC%A0%E4%B8%89%22"
/**
 * 将字符串格式化为UTF8编码的字节
 */
const toUTF8 = function (str, isGetBytes) {
      var back = [];
      var byteSize = 0;
      for (var i = 0; i < str.length; i++) {
          var code = str.charCodeAt(i);
          if (0x00 <= code && code <= 0x7f) {
                byteSize += 1;
                back.push(code);
          } else if (0x80 <= code && code <= 0x7ff) {
                byteSize += 2;
                back.push((192 | (31 & (code >> 6))));
                back.push((128 | (63 & code)))
          } else if ((0x800 <= code && code <= 0xd7ff)
                  || (0xe000 <= code && code <= 0xffff)) {
                byteSize += 3;
                back.push((224 | (15 & (code >> 12))));
                back.push((128 | (63 & (code >> 6))));
                back.push((128 | (63 & code)))
          }
       }
       for (i = 0; i < back.length; i++) {
            back[i] &= 0xff;
       }
       if (isGetBytes) {
            return back
       }
       if (byteSize <= 0xff) {
            return [0, byteSize].concat(back);
       } else {
            return [byteSize >> 8, byteSize & 0xff].concat(back);
        }
}

toUTF8(''); // =>  [0, 3, 228, 184, 165]

/**
 * 读取UTF8编码的字节,并转为Unicode的字符串
 */
const fromUTF8 = function (arr) {
    if (typeof arr === 'string') {
        return arr;
    }
    var UTF = '', _arr = arr;
    for (var i = 0; i < _arr.length; i++) {
        var one = _arr[i].toString(2),
                v = one.match(/^1+?(?=0)/);
        if (v && one.length == 8) {
            var bytesLength = v[0].length;
            var store = _arr[i].toString(2).slice(7 - bytesLength);
            for (var st = 1; st < bytesLength; st++) {
                store += _arr[st + i].toString(2).slice(2)
            }
            UTF += String.fromCharCode(parseInt(store, 2));
            i += bytesLength - 1
        } else {
            UTF += String.fromCharCode(_arr[i])
        }
    }
    return UTF
}

fromUTF8([0, 3, 228, 184, 165]); // => '严'

参考资料:

  1. 阮一峰的博客: https//www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
  2. segmentfault上张亚涛的专栏: https://segmentfault.com/a/1190000005794963