零散专题21 字符编码笔记

字符编码笔记学习笔记。

ASCII码

字节:八个二进制位,可以组合出256种状态,这称为一个字节(byte)。一个字节一共可以表示256种状态。

ASCII码是美国制定的,英语字符与二进制位之间的关系,一个规定了128个字符,比如空格SPACE320010 0000),大写字母A650100 0001)。这128个字符只占据了一个字节的后七位,最前面的一位统一规定为0

非ASCII码

英语用128个字符编码就够了,但是非英语字符却远远不够。一些语言使用了ASCII中闲置的第一位来编入新的符号,这样一些欧洲国家的编码体系可以最多表示256个字符。

但是对于汉语,还是不够的,需要使用多个字节表达一个符号,简体中文最常见的编码方式是GB2312,用两个字节表示一个汉字,所以理论上最多可以表示256 x 256 = 63356个字符。

Unicode

由于世界上存在多个编码方式,同一个二进制字符在不同编码方式下可以被解释成不同的符号。因此想要打开一个文件,就必须知道它的编码方式,否则使用错误的编码方式就会出现乱码。

Unicode是一种所有字符的编码方式,将世界上所有的符号纳入其中,每个字符都给予一个独一无二的编码。

Unicode现在可容纳100多个万个符号,每个符号编码都不一样。符号对应表可以查询unicode.org

Unicode的符号不是一次性定义的,而是分区定义的,每个区可以放65536($2^{16}$)字符,成为一个平面(plane),目前一个有17($2^{5}$)平面,也就是说,整个Uniocde字符集的大小是($2^{21}$)。

最前面的65536个字符,称为基本平面(BMP),码点范围是从0$2^{16}-1$,写成16进制就是从0000FFFF,所有常见字符都放在这个平面。剩下的字符都放在辅助平面(SMP)。

Unicode的问题

Unicode只是一个符号集,之规定了符号的二进制代码,却没有规定到底用什么样的字节序表示这个码点。

比如汉字的Unicode是十六进制数4E25,二进制是100111000100101,这个符号表示至少需要2个字节。

这样引出了两个问题:

  1. 如何让计算机区分Unicode和ASCII?计算机如何知道三个字节是Unicode中表示一个符号,而不是ASCII中的三个字符?
  2. 英文字节只用一个字节表示就够了,那如果统一长度字节表示,会造成很大的浪费

UTF-32

UTF-32是最直观的编码方法,每个码点用四个字节表示,字节内容一一对应码点。前位用0补齐。

它的优点是查找效率高,时间复杂度$O(1)$

它的缺点是浪费空间。同样的英语文本,比ASCII码大了4倍。

UTF-8

为了节省空间,UTF-8出现了。

UTF-8是在互联网使用最广泛的Unicode实现方式之一,其他实现方式还有UTF-16(字符用两个字节或四个字节表示)、UTF-32(字符用四个字节表示),不过在互联网环境基本不用。

UTF-8是Unicode的实现方式之一

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

UTF-8的编码规则:

  1. 对于单字节的字符,字节的第一位设为0,后面7位是这个符号的Unicode码。因此对于英语字符,Unicode编码和ASCII码是相同的。
  2. 对于n字节(n > 1),第一个字节的前n位都设为1,第n+1为设为0,后面字节的前两位一律设为10,其余的二进制位,全部为这个符号的Unicode码
1
2
3
4
5
6
7
unicode符号范围     |        UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0x0000 - 0x007F | 0xxxxxxx
0x0080 - 0x07FF | 110xxxxx 10xxxxxx
0x0800 - 0xFFFF | 1110xxxx 10xxxxxx 10xxxxxx
0x010000 - 0x10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

解读UTF-8编码时,如果一个字节的第一位是0,那么这个字节单独就是一个字符,如果第一位是1,则连续有多少个1,就表示当前字符占据多少个字节

例如,的Unicode编码是4E25(16进制),根据上表(实际自己尝试也会发现),它的UTF-8表示需要三个字节,即格式为1110xxxx 10xxxxxx 10xxxxxx,然后将的最后一个二进制位开始,从后向前依次填入格式中的x,多位补0,就得到了UTF-8编码11100100 10111000 10100101,转换为十六进制是E4B8A5

1
parseInt('111001001011100010100101', 2).toString(16).toUpperCase()

UTF-16

UTF-16介于UTF-8和UTF-32之间,结合了定长和变长两种编码方法的特点。

它的编码规则是,基本平面的字符占用两个字节,辅助平面字符占用4个字节。也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000U+10FFFF)。

UCS-2编码

JavaScript采用Unicode字符集,但是只支持UCS-2这一种编码方法

UCS-2的出现是由于历史上两只开发统一字符集的团队,在互相妥协、融合后的产物。UCS-2使用两个字节来表示基本平面的字符,没有对辅助平面字符处理。UTF-16的基本平面沿用UCS-2的编码,辅助平明字符使用4个自己表示。

UCS-2和UTF-16的关系是:UTF-16取代了UCS-2(UTF-16是UCS-2的超集),或者说UCS-2整合进了UTF-16。

由于JavaScript诞生时UTF-16还没有出现,所以只能采取UCS-2

JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字符如果是4个字节的字符(即辅助平面上的字符),会当做两个双字节的字符处理。JavaScript的字符函数都收到这一点的影响,无法返回正确结果。

为了解决这个问题,必须对码点进行判断,然后手动调整。在遍历字符串时判断码点,如果落在0xD8000xDBFF的区间,就要连同后面2个字节一起读取:

1
2
3
4
5
6
7
8
while (++index < length) {
// ...
if (charCode >= 0xD800 && charCode <= 0xDBFF) {
output.push(character + string.charAt(++index));
} else {
output.push(character);
}
}

类似的问题存在于所有的JavaScript字符操作函数,例如replace/substring/slice,这些函数都只对2字节的码点有效。要正确处理4字节的码点,就必须判断当前字符的码点范围。

ECMAScript 6

ES6大幅增强了对Unicode的支持,基本解决了上述问题。

(1)正确识别字符

ES6可以自动识别4字节的码点,因此遍历字符串就简单多了:

1
2
3
for (let s of string ) {
// ...
}

但是为了保证兼容性,length属性还是原来的行为方式。为了得到正确的字符串的长度,可以用下面的方式:

1
Array.from(string).length

比如,𝌆的码点为U+1D306,转换为UTF-16的编码是0xD834 DF06,长度是4个字节。length属性保持了以前的行为表现,认为2个字节是一个字符,所以认为𝌆的长度是2:

1
'𝌆`.length; // 2

正确的做法是:

1
Array.from('𝌆').length; // 1

(2)码点表示法

JavaScript允许直接使用码点表示Unicode字符,写法是:反斜杠 + u + 码点

1
'好' === '\u597D' // true

但是这样只能表示两个字节的码点,ES6修复了这个问题,只要将码点放在大括号内,就能识别4个字节的码点:

1
2
'\u{1d306}'
// "𝌆"

(3)ES6新增了几个专门处理4字节码点的方法:

1
2
3
4
5
String.fromCodePoint()  // 从Unicode码点返回对应字符
String.fromCodePoint('0x1d306') // "𝌆"

String.prototype.codePointAt // 从字符返回对应的码点
"𝌆".codePointAt(0).toString(16) // "1d306"

(4)正则表达式

ES6提供了u修饰符,对正则表达式提供4字节码点的支持

1
2
3
4
5
/^.$/.test('𝌆')
// false

/^.$/u.test('𝌆')
// true

参考