零散专题21 字符编码笔记
字符编码笔记学习笔记。
ASCII码
字节:八个二进制位,可以组合出256种状态,这称为一个字节(byte)。一个字节一共可以表示256种状态。
ASCII码是美国制定的,英语字符与二进制位之间的关系,一个规定了128个字符,比如空格SPACE
是32
(0010 0000
),大写字母A
是65
(0100 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进制就是从0000
到FFFF
,所有常见字符都放在这个平面。剩下的字符都放在辅助平面(SMP)。
Unicode的问题
Unicode只是一个符号集,之规定了符号的二进制代码,却没有规定到底用什么样的字节序表示这个码点。
比如汉字严
的Unicode是十六进制数4E25
,二进制是100111000100101
,这个符号表示至少需要2个字节。
这样引出了两个问题:
- 如何让计算机区分Unicode和ASCII?计算机如何知道三个字节是Unicode中表示一个符号,而不是ASCII中的三个字符?
- 英文字节只用一个字节表示就够了,那如果统一长度字节表示,会造成很大的浪费
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的编码规则:
- 对于单字节的字符,字节的第一位设为
0
,后面7位是这个符号的Unicode码。因此对于英语字符,Unicode编码和ASCII码是相同的。 - 对于
n
字节(n > 1
),第一个字节的前n
位都设为1
,第n+1
为设为0
,后面字节的前两位一律设为10
,其余的二进制位,全部为这个符号的Unicode码
1 | unicode符号范围 | UTF-8编码方式 |
解读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+010000
到U+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的字符函数都收到这一点的影响,无法返回正确结果。
为了解决这个问题,必须对码点进行判断,然后手动调整。在遍历字符串时判断码点,如果落在0xD800
和0xDBFF
的区间,就要连同后面2个字节一起读取:
1 | while (++index < length) { |
类似的问题存在于所有的JavaScript字符操作函数,例如replace
/substring
/slice
,这些函数都只对2字节的码点有效。要正确处理4字节的码点,就必须判断当前字符的码点范围。
ECMAScript 6
ES6大幅增强了对Unicode的支持,基本解决了上述问题。
(1)正确识别字符
ES6可以自动识别4字节的码点,因此遍历字符串就简单多了:
1 | 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 | '\u{1d306}' |
(3)ES6新增了几个专门处理4字节码点的方法:
1 | String.fromCodePoint() // 从Unicode码点返回对应字符 |
(4)正则表达式
ES6提供了u
修饰符,对正则表达式提供4字节码点的支持
1 | /^.$/.test('𝌆') |