一、ASCII 码
一个字节(byte)有8个bit位,每个位有0和1两种状态,一个字可以表示2^8共有256种不同状态
ASCII 码一共规定了128个字符的编码,比如空格SPACE
是32(二进制00100000
),大写的字母A
是65(二进制01000001
)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0
。
ASCII 码表:
Bin(二进制)
| Oct(八进制) |
Dec(十进制)
|
Hex(十六进制)
|
缩写/字符
|
解释
|
0000 0000
|
0
|
0
|
00
|
NUT(null)
|
空字符
|
0000 0001
|
1
|
1
|
01
|
SOH(start of headline)
|
标题开始
|
0000 0010
|
2
|
2
|
02
|
STX (start of text)
|
正文开始
|
0000 0011
|
3
|
3
|
03
|
ETX (end of text)
|
正文结束
|
0000 0100
|
4
|
4
|
04
|
EOT (end of transmission)
|
传输结束
|
0000 0101
|
5
|
5
|
05
|
ENQ (enquiry)
|
请求
|
0000 0110
|
6
|
6
|
06
|
ACK (acknowledge)
|
收到通知
|
0000 0111
|
7
|
7
|
07
|
BEL (bell)
|
响铃
|
0000 1000
|
10
|
8
|
08
|
BS (backspace)
|
退格
|
0000 1001
|
11
|
9
|
09
|
HT (horizontal tab)
|
水平制表符
|
0000 1010
|
12
|
10
|
0A
|
LF (NL line feed, new line)
|
换行键
|
0000 1011
|
13
|
11
|
0B
|
VT (vertical tab)
|
垂直制表符
|
0000 1100
|
14
|
12
|
0C
|
FF (NP form feed, new page)
|
换页键
|
0000 1101
|
15
|
13
|
0D
|
CR (carriage return)
|
回车键
|
0000 1110
|
16
|
14
|
0E
|
SO (shift out)
|
不用切换
|
0000 1111
|
17
|
15
|
0F
|
SI (shift in)
|
启用切换
|
0001 0000
|
20
|
16
|
10
|
DLE (data link escape)
|
数据链路转义
|
0001 0001
|
21
|
17
|
11
|
DC1 (device control 1)
|
设备控制1
|
0001 0010
|
22
|
18
|
12
|
DC2 (device control 2)
|
设备控制2
|
0001 0011
|
23
|
19
|
13
|
DC3 (device control 3)
|
设备控制3
|
0001 0100
|
24
|
20
|
14
|
DC4 (device control 4)
|
设备控制4
|
0001 0101
|
25
|
21
|
15
|
NAK (negative acknowledge)
|
拒绝接收
|
0001 0110
|
26
|
22
|
16
|
SYN (synchronous idle)
|
同步空闲
|
0001 0111
|
27
|
23
|
17
|
ETB (end of trans. block)
|
结束传输块
|
0001 1000
|
30
|
24
|
18
|
CAN (cancel)
|
取消
|
0001 1001
|
31
|
25
|
19
|
EM (end of medium)
|
媒介结束
|
0001 1010
|
32
|
26
|
1A
|
SUB (substitute)
|
代替
|
0001 1011
|
33
|
27
|
1B
|
ESC (escape)
|
换码(溢出)
|
0001 1100
|
34
|
28
|
1C
|
FS (file separator)
|
文件分隔符
|
0001 1101
|
35
|
29
|
1D
|
GS (group separator)
|
分组符
|
0001 1110
|
36
|
30
|
1E
|
RS (record separator)
|
记录分隔符
|
0001 1111
|
37
|
31
|
1F
|
US (unit separator)
|
单元分隔符
|
0010 0000
|
40
|
32
|
20
|
(space)
|
空格
|
0010 0001
|
41
|
33
|
21
|
!
| 叹号 |
0010 0010
|
42
|
34
|
22
|
"
| 双引号 |
0010 0011
|
43
|
35
|
23
|
#
| 井号 |
0010 0100
|
44
|
36
|
24
|
$
| 美元符 |
0010 0101
|
45
|
37
|
25
|
%
| 百分号 |
0010 0110
|
46
|
38
|
26
|
&
| 和号 |
0010 0111
|
47
|
39
|
27
|
'
| 闭单引号 |
0010 1000
|
50
|
40
|
28
|
(
|
开括号
|
0010 1001
|
51
|
41
|
29
|
)
|
闭括号
|
0010 1010
|
52
|
42
|
2A
|
*
| 星号 |
0010 1011
|
53
|
43
|
2B
|
+
| 加号 |
0010 1100
|
54
|
44
|
2C
|
,
| 逗号 |
0010 1101
|
55
|
45
|
2D
|
-
| 减号/破折号 |
0010 1110
|
56
|
46
|
2E
|
.
| 句号 |
00101111
|
57
|
47
|
2F
|
/
| 斜杠 |
00110000
|
60
|
48
|
30
|
0
| 数字0 |
00110001
|
61
|
49
|
31
|
1
| 数字1 |
00110010
|
62
|
50
|
32
|
2
| 数字2 |
00110011
|
63
|
51
|
33
|
3
| 数字3 |
00110100
|
64
|
52
|
34
|
4
| 数字4 |
00110101
|
65
|
53
|
35
|
5
| 数字5 |
00110110
|
66
|
54
|
36
|
6
| 数字6 |
00110111
|
67
|
55
|
37
|
7
| 数字7 |
00111000
|
70
|
56
|
38
|
8
| 数字8 |
00111001
|
71
|
57
|
39
|
9
| 数字9 |
00111010
|
72
|
58
|
3A
|
:
| 冒号 |
00111011
|
73
|
59
|
3B
|
;
| 分号 |
00111100
|
74
|
60
|
3C
|
<
| 小于 |
00111101
|
75
|
61
|
3D
|
=
| 等号 |
00111110
|
76
|
62
|
3E
|
>
| 大于 |
00111111
|
77
|
63
|
3F
|
?
| 问号 |
01000000
|
100
|
64
|
40
|
@
| 电子邮件符号 |
01000001
|
101
|
65
|
41
|
A
| 大写字母A |
01000010
|
102
|
66
|
42
|
B
| 大写字母B |
01000011
|
103
|
67
|
43
|
C
| 大写字母C |
01000100
|
104
|
68
|
44
|
D
| 大写字母D |
01000101
|
105
|
69
|
45
|
E
| 大写字母E |
01000110
|
106
|
70
|
46
|
F
| 大写字母F |
01000111
|
107
|
71
|
47
|
G
| 大写字母G |
01001000
|
110
|
72
|
48
|
H
| 大写字母H |
01001001
|
111
|
73
|
49
|
I
| 大写字母I |
01001010
|
112
|
74
|
4A
|
J
| 大写字母J |
01001011
|
113
|
75
|
4B
|
K
| 大写字母K |
01001100
|
114
|
76
|
4C
|
L
| 大写字母L |
01001101
|
115
|
77
|
4D
|
M
| 大写字母M |
01001110
|
116
|
78
|
4E
|
N
| 大写字母N |
01001111
|
117
|
79
|
4F
|
O
| 大写字母O |
01010000
|
120
|
80
|
50
|
P
| 大写字母P |
01010001
|
121
|
81
|
51
|
Q
| 大写字母Q |
01010010
|
122
|
82
|
52
|
R
| 大写字母R |
01010011
|
123
|
83
|
53
|
S
| 大写字母S |
01010100
|
124
|
84
|
54
|
T
| 大写字母T |
01010101
|
125
|
85
|
55
|
U
| 大写字母U |
01010110
|
126
|
86
|
56
|
V
| 大写字母V |
01010111
|
127
|
87
|
57
|
W
| 大写字母W |
01011000
|
130
|
88
|
58
|
X
| 大写字母X |
01011001
|
131
|
89
|
59
|
Y
| 大写字母Y |
01011010
|
132
|
90
|
5A
|
Z
| 大写字母Z |
01011011
|
133
|
91
|
5B
|
[
| 开方括号 |
01011100
|
134
|
92
|
5C
|
\
| 反斜杠 |
01011101
|
135
|
93
|
5D
|
]
| 闭方括号 |
01011110
|
136
|
94
|
5E
|
^
| 脱字符 |
01011111
|
137
|
95
|
5F
|
_
| 下划线 |
01100000
|
140
|
96
|
60
|
`
| 开单引号 |
01100001
|
141
|
97
|
61
|
a
| 小写字母a |
01100010
|
142
|
98
|
62
|
b
| 小写字母b |
01100011
|
143
|
99
|
63
|
c
| 小写字母c |
01100100
|
144
|
100
|
64
|
d
| 小写字母d |
01100101
|
145
|
101
|
65
|
e
| 小写字母e |
01100110
|
146
|
102
|
66
|
f
| 小写字母f |
01100111
|
147
|
103
|
67
|
g
| 小写字母g |
01101000
|
150
|
104
|
68
|
h
| 小写字母h |
01101001
|
151
|
105
|
69
|
i
| 小写字母i |
01101010
|
152
|
106
|
6A
|
j
| 小写字母j |
01101011
|
153
|
107
|
6B
|
k
| 小写字母k |
01101100
|
154
|
108
|
6C
|
l
| 小写字母l |
01101101
|
155
|
109
|
6D
|
m
| 小写字母m |
01101110
|
156
|
110
|
6E
|
n
| 小写字母n |
01101111
|
157
|
111
|
6F
|
o
| 小写字母o |
01110000
|
160
|
112
|
70
|
p
| 小写字母p |
01110001
|
161
|
113
|
71
|
q
| 小写字母q |
01110010
|
162
|
114
|
72
|
r
| 小写字母r |
01110011
|
163
|
115
|
73
|
s
| 小写字母s |
01110100
|
164
|
116
|
74
|
t
| 小写字母t |
01110101
|
165
|
117
|
75
|
u
| 小写字母u |
01110110
|
166
|
118
|
76
|
v
| 小写字母v |
01110111
|
167
|
119
|
77
|
w
| 小写字母w |
01111000
|
170
|
120
|
78
|
x
| 小写字母x |
01111001
|
171
|
121
|
79
|
y
| 小写字母y |
01111010
|
172
|
122
|
7A
|
z
| 小写字母z |
01111011
|
173
|
123
|
7B
|
{
| 开花括号 |
01111100
|
174
|
124
|
7C
|
|
| 垂线 |
01111101
|
175
|
125
|
7D
|
}
| 闭花括号 |
01111110
|
176
|
126
|
7E
|
~
| 波浪号 |
01111111
|
177
|
127
|
7F
|
DEL (delete)
|
删除
|
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é
的编码为130(二进制10000010
)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é
,在希伯来语编码中却代表了字母Gimel
(ג
),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。
二. Unicode
如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字都表示的,这是一种所有符号的编码。汉字对应Unicode编码。
Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字严
的 Unicode 是十六进制数4E25
,转换成二进制数足足有15位(100111000100101
),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0
,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
Unicode只规定了每个字符的码点,到底用什么样的字节序表示这个码点,就涉及到编码方法。
最直观的编码方法是,每个码点使用四个字节表示,字节内容一一对应码点。这种编码方法就叫做UTF-32。比如,码点0就用四个字节的0表示,码点597D就在前面加两个字节的0。
UTF-32的优点在于,转换规则简单直观,查找效率高。缺点在于浪费空间,同样内容的英语文本,它会比ASCII编码大四倍。这个缺点很致命,导致实际上没有人使用这种编码方法,HTML 5标准就明文规定,网页不得编码成UTF-32。
三、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 码。
下表总结了编码规则,字母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 复制代码
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0
,则这个字节单独就是一个字符;如果第一位是1
,则连续有多少个1
,就表示当前字符占用多少个字节。
下面,还是以汉字严
为例,演示如何实现 UTF-8 编码。
严
的 Unicode 是4E25
(100111000100101
),根据上表,可以发现4E25
处在第三行的范围内(0000 0800 - 0000 FFFF
),因此严
的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx
。然后,从严
的最后一个二进制位开始,依次从后向前填入格式中的x
,多出的位补0
。这样就得到了,严
的 UTF-8 编码是11100100 10111000 10100101
,转换成十六进制就是E4B8A5
。
四、UTF-16
UTF-16编码介于UTF-32与UTF-8之间,同时结合了定长和变长两种编码方法的特点。
它的编码规则很简单:基本平面的字符占用2个字节,辅助平面的字符占用4个字节。也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。
编码范围 | 字节
----------------------+---------------------------------------------
0x0000-0xFFFF | 2
0x010000-0x10FFFF | 4
复制代码
于是就有一个问题,当我们遇到两个字节,怎么看出它本身是一个字符,还是需要跟其他两个字节放在一起解读?
说来很巧妙,我也不知道是不是故意的设计,从U+D800到U+DFFF是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。具体来说,辅助平面的字符位共有220个,也就是说,对应这些字符至少需要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使用哪一种编码?
JavaScript语言采用Unicode字符集,但是只支持一种编码方法。
这种编码既不是UTF-16,也不是UTF-8,更不是UTF-32。上面那些编码方法,JavaScript都不用。
JavaScript用的是UCS-2!
六、UCS-2编码
怎么突然杀出一个UCS-2?这就需要讲一点历史。
互联网还没出现的年代,曾经有两个团队,不约而同想搞统一字符集。一个是1988年成立的Unicode团队,另一个是1989年成立的UCS团队。等到他们发现了对方的存在,很快就达成一致:世界上不需要两套统一字符集。
1991年10月,两个团队决定合并字符集。也就是说,从今以后只发布一套字符集,就是Unicode,并且修订此前发布的字符集,UCS的码点将与Unicode完全一致。
UCS的开发进度快于Unicode,1990年就公布了第一套编码方法UCS-2,使用2个字节表示已经有码点的字符。(那个时候只有一个平面,就是基本平面,所以2个字节就够用了。)UTF-16编码迟至1996年7月才公布,明确宣布是UCS-2的超集,即基本平面字符沿用UCS-2编码,辅助平面字符定义了4个字节的表示方法。
两者的关系简单说,就是UTF-16取代了UCS-2,或者说UCS-2整合进了UTF-16。所以,现在只有UTF-16,没有UCS-2。
七、JavaScript的诞生背景
那么,为什么JavaScript不选择更高级的UTF-16,而用了已经被淘汰的UCS-2呢?
答案很简单:非不想也,是不能也。因为在JavaScript语言出现的时候,还没有UTF-16编码。
1995年5月,Brendan Eich用了10天设计了JavaScript语言;10月,第一个解释引擎问世;次年11月,Netscape正式向ECMA提交语言标准(整个过程详见《JavaScript诞生记》)。对比UTF-16的发布时间(1996年7月),就会明白Netscape公司那时没有其他选择,只有UCS-2一种编码方法可用!
八、JavaScript字符函数的局限
由于JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。JavaScript的字符函数都受到这一点的影响,无法返回正确结果。
还是以字符为例,它的UTF-16编码是4个字节的0xD834 DF06。问题就来了,4个字节的编码不属于UCS-2,JavaScript不认识,只会把它看作单独的两个字符U+D834和U+DF06。前面说过,这两个码点是空的,所以JavaScript会认为是两个空字符组成的字符串!
上面代码表示,JavaScript认为字符的长度是2,取到的第一个字符是空字符,取到的第一个字符的码点是0xDB34。这些结果都不正确!
解决这个问题,必须对码点做一个判断,然后手动调整。下面是正确的遍历字符串的写法。
while (++index < length) {
// ...
if (charCode >= 0xD800 && charCode <= 0xDBFF) {
output.push(character + string.charAt(++index));
} else {
output.push(character);
}
}
复制代码
上面代码表示,遍历字符串的时候,必须对码点做一个判断,只要落在0xD800到0xDBFF的区间,就要连同后面2个字节一起读取。
类似的问题存在于所有的JavaScript字符操作函数。
- String.prototype.replace()
- String.prototype.substring()
- String.prototype.slice()
- ...
上面的函数都只对2字节的码点有效。要正确处理4字节的码点,就必须逐一部署自己的版本,判断一下当前字符的码点范围。
九、Little endian 和 Big endian
上一节已经提到,UCS-2 格式可以存储 Unicode 码(码点不超过0xFFFF
)。以汉字严
为例,Unicode 码是4E25
,需要用两个字节存储,一个字节是4E
,另一个字节是25
。存储的时候,4E
在前,25
在后,这就是 Big endian 方式;25
在前,4E
在后,这是 Little endian 方式。
这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。
第一个字节在前,就是"大头方式"(Big endian),第二个字节在前就是"小头方式"(Little endian)。
那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?
Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用FEFF
表示。这正好是两个字节,而且FF
比FE
大1
。
如果一个文本文件的头两个字节是FE FF
,就表示该文件采用大头方式;如果头两个字节是FF FE
,就表示该文件采用小头方式。
十、ECMAScript 6
JavaScript的下一个版本ECMAScript 6(简称ES6),大幅增强了Unicode支持,基本上解决了这个问题。
(1)正确识别字符
ES6可以自动识别4字节的码点。因此,遍历字符串就简单多了。
for (let s of string ) {
// ...
}
复制代码
但是,为了保持兼容,length属性还是原来的行为方式。为了得到字符串的正确长度,可以用下面的方式。
Array.from(string).length 复制代码
(2)码点表示法
JavaScript允许直接用码点表示Unicode字符,写法是"反斜杠+u+码点"。
'好' === '\u597D' // true
复制代码
但是,这种表示法对4字节的码点无效。ES6修正了这个问题,只要将码点放在大括号内,就能正确识别。
(3)字符串处理函数
ES6新增了几个专门处理4字节码点的函数。
- String.fromCodePoint():从Unicode码点返回对应字符
- String.prototype.codePointAt():从字符返回对应的码点
- String.prototype.at():返回字符串给定位置的字符
(4)正则表达式
ES6提供了u修饰符,对正则表达式添加4字节码点的支持。
(5)Unicode正规化
有些字符除了字母以外,还有附加符号。比如,汉语拼音的Ǒ,字母上面的声调就是附加符号。对于许多欧洲语言来说,声调符号是非常重要的。
Unicode提供了两种表示方法。一种是带附加符号的单个字符,即一个码点表示一个字符,比如Ǒ的码点是U+01D1;另一种是将附加符号单独作为一个码点,与主体字符复合显示,即两个码点表示一个字符,比如Ǒ可以写成O(U+004F) + ˇ(U+030C)。
// 方法一
'\u01D1'
// 'Ǒ'
// 方法二
'\u004F\u030C'
// 'Ǒ'
复制代码
这两种表示方法,视觉和语义都完全一样,理应作为等同情况处理。但是,JavaScript无法辨别。
'\u01D1'==='\u004F\u030C'
//false
复制代码
ES6提供了normalize方法,允许"Unicode正规化",即将两种方法转为同样的序列。
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true
复制代码