JavaScript的字符编码
# 一、字符集
各个国家文字、字母、标点符号等等的集合,成为字符集。
# ASCII 字符集
ASCII 字符集:计算机最初流行在欧美国家,针对他们的字符集合,主要是拉丁字母和阿拉伯数字。一共规定了 128 个字符以及对应的二进制转换关系,128 个字符包括了可现实的 26 个大小写字母、10 个数字、标点符号以及特殊的控制符。这 128 个字符用一个字节(8bit)来表示绰绰有余,一个字节可以表示 256 个字符,所以 ASCII 编码当前只利用了字节的 7 为,最高位统一置 0。
# Unicode 字符集
Unicode 字符集:ASCII 字符集具有很大的局限性,只能诸多表示 127 个字符,而没有考虑汉字、阿拉伯语等文字字符。Unicode 是全世界所有字符的一个集合。每个字符对应一个数字,每个数字对应一个字符,而这个数字也称之为码点,根据码点就可以从字符集中索引到对应的字符。
Unicode 字符集将全世界所有的字符包含在了一个集合里面,可以近似认为其中三分之二以上都来自东亚文字。这么多符号,Unicode 不是一次性定义的,而是分区定义。每个区可以存放 65536 个(2^16)字符,称为一个平面(plane)。目前,一共有 17 个(2^5)平面,也就是说,整个 Unicode 字符集的大小现在是 2^21。
最前面的 65536 个字符位,称为基本平面(缩写 BMP),它的码点范围是从 0 一直到 2^16-1,写成 16 进制就是从 U+0000 到 U+FFFF。所有最常见的字符都放在这个平面,这是 Unicode 最先定义和公布的一个平面。
剩下的字符都放在辅助平面(缩写 SMP),码点范围从 U+010000 一直到 U+10FFFF。
# 二、字符编码
先来说一下什么是字符编码呢?我们计算机传输和存储的数据都是二进制的,对于字符也是如此。把一个数值与字符集中的字符进行唯一匹配,对应的规则就称为字符编码。上面提到的字符集,会有一套与二进制数值对应的规则,即为对应的 ASCII 编码、Unicode 编码等等。
Unicode 编码方式的优点是它涵盖了所有的字符集合,缺点是因为涵盖了所有的字符集合,所有它相比于 ASCII 编码占用的字节多。比如说 ASCII 编码中一个字符只占 1 字节,而同样的字符在 Unicode 编码中至少占据 3 个字节,而我们并不会用的 Unicode 中所有的字符,常用的字符都分布在一个平面上,直接使用 Unicode 来表示字符就太浪费存储空间和传输流量了。
于是便出现了 UTF-8、16、32 等编码方式(Unicode Transformation Format),用来平衡 Unicode 所有平面字符的存储空间、兼容和解码问题。我们常见的 UTF-8 就是一种字符编码方式,它是针对 Unicode 的可变长度字符编码,也是一种前缀码。它可以用来表示 Unicode 标准中的任何字符,且其编码中的第一个字节仍然兼容 ASCII 字符集。
提示
Unicode 只规定了每个字符的码点,到底用什么样的字节序列表示这个码点,就涉及到了编码方法。对 Unicode 编码的方法有 UTF--8、UTF-16 和 UTF-32。
# UTF-32 编码
这是最直观的编码方法,每个码点都使用 4 个字节来表示,字节内容与码点一一对应,这就是 UTF-32。
比如说:码点 0 就用四个字节的 0 表示,码点 597D 就在前面加两个字节的 0。
U+0000 = 0x0000 0000
U+597D = 0x0000 597D
2
3
特点:
- 查找效率高,时间复杂度 o(1);
- 浪费空间,比相同的 ACII 编码文件大四倍;
- HTML5 标准明文规定,网页不得编码成 UTF-32。
# UTF-8 编码
UTF-8 编码方式可以节省空间,它是变长的编码方法,字符长度从 1 一个字节到 4 个字节不等。越是常用的字符,字节越短,最前面的 128 个字符,只使用 1 个字节表示,与 ASCII 码完全相同。
UTF-8 编码方式:
- 对于单字节字符,字节第一位置零,后 7 位使用该字符 Unicode 码点。即该字符编码同 ASCII 码一致
- 对于 n(n > 1)个字节字符,最高位字节,前 n 位置 1,第 n + 1 位置 0,剩余低字节均以 10 开头,有效的二进制位(下图中的 x 占用位)则表示该字符的 Unicode 码点。
Unicode 码点范围(十六进制) | UTF-8 编码方式(二进制) | 字节数 |
---|---|---|
U+0000 U+007F(0~127) | 0xxx xxxx | 单字节 |
U+0080 U+07FF(128~2047) | 110x xxxx 10xx xxxx | 双字节 |
U+0800 U+FFFF(2048~65535) | 1110 xxxx 10xx xxxx 10xx xxxx | 三字节 |
U+10000 U+10FFFF(65536~2097151) | 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx | 四字节 |
# UTF-16 编码
UTF-16 编码介于 UTF-32 与 UTF-8 之间,同时结合了定长和变长两种编码方法的特点。
它的编码规则:基本平面的字符占用 2 个字节,辅助平面的字符占用 4 个字节。即 UTF-16 的编码长度要么是 2 个字节(U+0000 到 U+FFFF),要么是 4 个字节(U+010000 到 U+10FFFF)
当 Unicode 码点在 U+0000 到 U+FFFF 范围内,则可以判断为基本平面字符,直接将码点转为对应的十六进制形式,长度为 2 个字节;
当 Unicode 码点在 U+010000 到 U+10FFFF范围内,则判断为辅助平面字符,会编码为4个字节。转换的规则是什么样的呢?
在基本平面内,从U+D800到U+DFFF是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。
具体来说,辅助平面的字符位共有2^20个,也就是说,对应这些字符至少需要20个二进制位。UTF-16将这20位拆成两半,前10位映射在U+D800到U+DBFF(空间大小210),称为高位(H),后10位映射在U+DC00到U+DFFF(空间大小210),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
所以,当我们遇到两个字节,发现它的码点在U+D800到U+DBFF之间,就可以断定,紧跟在后面的两个字节的码点,应该在U+DC00到U+DFFF之间,这四个字节必须放在一起解读。
# 三、JavaScript 编码
# JS 采用哪一种编码
JavaScript 语言采用 Unicode 字符集,但是只支持一种编码方法。这种编码不是 UTF-16、8、32,用的是 UCS-2!
因此有两个团队都想来搞统一字符集,一个是 Unicode,另外一个就是 UCS-2 字符集;当他们发现彼此之后,决定合并成一套字符集,就是 Unicode。但是呢,UCS 的开发速度快于 Unicode,开发了第一套编码方法 UCS-2,使用 2 个字节表示已经有的码点字符。
而 Unicode 的编码方式之一 UTF-16 编码晚于 UCS-2 编码方法,即 UTF-16 宣布是 UCS-2 的超集;即 UTF-16 取代了 UCS-2,也可以说 UCS-2 被整合进了 UTF-16。
# 为什么 JS 选择已经淘汰的 UCS-2 编码方式
为什么 JavaScript 不选用更高级的 UTF-16 呢?因为 JavaScript 语言出现的以后,根本没有 UTF-16 编码,JS 语言发明者只用了 10 天设计了 JavaScript 语言,随后出现了解释引擎。
# JS 字符函数的局限
因为 JS 采用 Unicode 字符集,但是只支持 16 位的 UTF-16 编码,不支持 32 位。
由于 JavaScript 只能处理 UCS-2 编码,造成所有字符在这门语言中都是 2 个字节,如果是 4 个字节的字符,会被当作 2 个双字节的字符处理。JavaScript 的字符函数都受到了这一点的影响,无法返回正确结果。
为了解决这个问题,必须对码点做一个判断,然后手动调整。下面是正确遍历字符串的方法:
while(++index <length) {
if(charCode >= 0xD800 && charCode <= 0xDBFF) {
output.push(character + String.charAt(++index));
}else {
output.push(character)
}
}
2
3
4
5
6
7
但在ES6版本,大幅增强了Unicode的支持,基本上解决了这个问题。
# 1. 正确识别字符串
ES6可以自动识别4字节的码点,可以通过for...of来遍历字符串:
let str = 's🏁s';
for(let s of str) {
console.log(s,s.length)
}
// s 1
// 🏁 2
// s 1
2
3
4
5
6
7
可以看到for...of可以正常遍历字符串,即使是占4个字节也可以正常遍历;但是为了保持兼容,length属性仍然还是原来的行为。为了得到字符串的正确长度,可以用以下方式:
console.log(str.length) // 4
console.log(Array.from(str1).length) // 3
2
# 2. 码点表示法
JavaScript允许直接用码点表示Unicode字符,写法是"反斜杠+u+码点"。
'好' === '\u597D' // true
但是,这种表示法对4字节的码点无效。ES6修正了这个问题,只要将码点放在大括号内,就能正确识别。或者用UTF-16编码的四个字节比较:
'😂' === '\u1f602' //false
'😂' === '\u{1f602}' //true
'😂'.charCodeAt(0) //55357
55357 .toString(16) //'d83d'
'😂'.charCodeAt(1) //56834
56834 .toString(16) //'de02'
'😂' === '\ud83d\ude02' //true
2
3
4
5
6
7
8
# 3. 字符串处理函数
ES6新增了几个专门处理4字节码点的函数。
- String.fromCodePoint():从Unicode码点返回对应字符
- String.prototype.codePointAt():从字符返回对应的码点
- String.prototype.at():返回字符串给定位置的字符
# 4. 正则表达式
ES6提供了u修饰符,对正则表达式添加4字节码点的支持。
参考文章: