unicode
unicode是一个字符集,给所有的符号、字母、汉字一个唯一编码的集合。这个编码就像一个id,我们叫做码点(code point), 格式写作U+FFFF或\uFFFF
例如
- U+0000 = null
- \u5409 = '吉'
- U+20BB7 = '𠮷'
- U+1F602 = 😂
unicode字符是分区定义的,每个区可以存放216个字符,称为一个平面。
其中U+0000 ~ U+FFFF 称为基本平面,也就是前216个字符。其他的字符范围是U+010000 ~ U+10FFFF, 称为辅助平面。
辅助平面一共16个 比如+010000 ~ U+01FFFF, U+020000 ~ U+02FFFF再或者U+F0000 ~ U+FFFF和U+100000 ~ U+10FFFF
unicode如何存储 - 编码方法
编码方法指的是这个字符的码点在字节中如何存储,因为一个码点可能需要多个字节存储。
那么读取字节的时候怎么知道当前字节是不是一个完整的字符呢?这需要一个约定好的编码方式让我们能够正确读出字节里的码点
utf-32
这是非常好理解的一种编码方法,固定用4个字节(32位)表示一个码点,字节内容就是unicode码点的十六进制值,长度不够的前面补上0。
比如吉的unicode码是U+5409,utf-32编码的十六进制写作0x00005409,字节中存储的就是00 00 54 09。
再比如字母A的unicode码是U+0041,utf-32编码的十六进制写作0x00000041,字节中的存储就是00 00 00 41。
这种编码方法的好处是可以非常直观的表示一个unicode码点,但就是比较浪费空间
utf-8
这是一种变长的编码方法,好处是非常节省空间。unicode码点小,用的字节数就少;unicode码点大,用的字节数就多,最小可以只用1个字节。
编码规则和表示范围如下:
| 字节数 | 字节码(二进制) | 可表示的unicode范围 |
|---|---|---|
| 1个字节 | 0xxxxxxx | 表示U+000000 ~ U+00007F |
| 2个字节 | 110xxxxx 10xxxxxx | 表示U+000080 ~ U+0007FF |
| 3个字节 | 1110xxxx 10xxxxxx 10xxxxxx | 表示U+000800 ~ U+00FFFF |
| 4个字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 表示U+010000 ~ U+10FFFF |
前缀0,110,10等都是标识位,标识这个字符有多少个字节的。x是真正有意义的码位
知道了以上编码规则我们就可以计算一个以utf-8编码的unicode字符的字节码了。
比如吉的unicode码点是U+5409:
| (16进制)U+ | 5 | 4 | 0 | 9 |
| (2进制)U+ | 0101 | 0100 | 0000 | 1001 |
| 匹配格式 | 1110xxxx 10xxxxxx 10xxxxxx | |||
| 分隔位 | 0101 010000 001001 | |||
| 放入位 | 11100101 10010000 10001001 | |||
| 转换成16进制 | E 5 9 0 8 9 | |||
这样就得到吉的utf-8字节码 0xE59089
winHex可以用来查看文件的字节,你可以创建一个
txt文件,里面就打一个吉,然后另存为utf-8,之后用winHex这个软件打开这个txt文件,就可以看到对应的E59089
当选择存储成
带有BOM的utf-8时, 会增加前缀字节EF BB BF
也可以通过unicode字符对照列表查看字节码
反过来由utf-8的字节码得到unicode也是一样,先把字节码转换成2进制,然后去除掉标志位,组合剩下的位数就是unicode码味的二进制了,再转换成16进制就可以了
可以尝试一下计算𠮷的utf-8字节码,0xF0A0AEB7
utf-16
这是一种变长和定长结合的编码方法。
基本平面中字符的编码是定长的,2个字节,完全对应 U+0000 ~ U+FFFF。比如 吉,unicode码点是U+5409, 则字节码就是 0x5409
辅助平面中字符的编码用4个字节表示,unicode码和字节码不能完全对应。比如𠮷,unicode码点是U+20BB7, 但字节码是0xD842DFB7。
那么读取一个字符时,怎么知道这2个字节是不是完整的字符呢?
utf-16规定了,如果这2个字节表示的值在0xD800 ~ 0xDFFF 之间, 就说明这个字节要和前面或者后面的一个字节一起共同组成一个字符。表示U+010000 ~ U+10FFFF(共 24 * 216= 220个字符)范围的unicode字符
从unicode字符对照列表中可以看到,U+D800 ~ U+DBFF 没有分配字符,叫做高位专用替代,是位于4字节中的前2个字节, U+DC00 ~ U+DFFF 也没有分配字符,叫做低位专用替代,是位于4字节中的后2个字节
uft-16编码时前两个字节和后两个字节都各存储10位有效码位。共20位二进制位,可以表示5位16进制,总共2(5*4) = 220 个字符,刚好对应表示U+010000 ~ U+10FFFF的数量
辅助平面字符( U+010000 ~ U+10FFFF)编码规则如下:
| 高位专用替代 | 低位专用替代 | |
| 字节码范围(十六进制) | 0xD800 ~ 0xDBFF | 0xDC00 ~ 0xDFFF |
| 字节码范围 (二进制) |
1101 1000 0000 0000 ~ 1101 1011 1111 1111 |
1101 1100 0000 0000 ~ 1101 1111 1111 1111 |
| 有效码位 | 1101 10xx xxxx xxxx | 1101 11xx xxxx xxxx |
即 U+010000 对应 1101 10 00 0000 0000
即 U+10FFFF 对应 1101 11 11 1111 1111
接下来尝试计算一个以utf-16编码的字符的字节码。 比如𠮷的unicode码点是U+20BB7:
| 𠮷 | U+20BB7 |
| 减去 U+010000 | U+10BB7 |
| 写作二进制 | 0001 0000 1011 1011 0111 |
| 分为两个10位 | 00 0100 0010 11 1011 0111 |
| 分别放入高低专用位的范围 | 1101 1000 0100 0010 1101 1111 1011 0111 |
| 得到字节 | D 8 4 2 D F B 7 |
这样就得到𠮷的utf-16字节码 0xD842DFB7
你可以在
txt输入一个𠮷,然后另存为utf-16 BE,之后用winHex这个软件打开这个txt文件,就可以看到对应的D8 42 DF B7
可以观察到字节中还有额外的
FE FF是用来表示字节顺序的,字节顺序标识了当读取每2个字节的时候是从左向右读还是从右向左读。
- 当选择
大端(BE)字节序存储时,utf-16编码中增加标识位FE FF,存储成D8 43 DF B7;- 当选择做
小端(LE)字节序存储时,utf-16编码中增加标识位FF FE,存储成42 D8 B7 DF
js中的处理
js支持的编码: UCS-2
UCS-2是ISO 10646标准定义的一种16位的编码形式,时间早于unicode,后来由于unicode严密的包含了UCS-2,并做了更多扩展,就和unicode合并了。
UCS-2支持的字符集只包括U+0000 ~ U+FFFF范围的字符,也就是只支持utf-16中 基本平面内的字符,不能识别超过2字节的字符
而js是基于UCS-2的,因此识别不了4字节字符,导致有一些方法在调用时候出错
es6支持自动识别4字节的码点 可以用以下方法正确遍历字符串和计算长度
// es5
for(let i=0; i< '𠮷'.length; i++) {
console.log('𠮷'[i]) // � �
}
// es6
for(let s of '𠮷') {
console.log(s) // '𠮷'
}
// es5
'𠮷'.length // 2
// es6
Array.from('𠮷').length // 1
es6用码点表示2字节和4字节
'\u{20bb7}' // '𠮷'
'\u5409' // 吉
es6专门处理4字节码点的函数
String.fromCodePoint():从Unicode码点返回对应字符
console.log(String.fromCodePoint(9731, 9733, 9842, 0x2F804,0x5409, 0x005C));
// expected output: "☃★♲你吉 \" 前面3个是10进制的
// es5
String.fromCharCode(9731,9733,9842,0x2f804,0x5409,0x005C) // "☃★♲吉\" // 显示不出5位16进制的"你"
String.prototype.codePointAt():从字符返回对应的码点
// 当文字是普通平面字符时(`u+0000 - u+FFFF`),codePointAt()和charCodeAt()返回了一样的结果
// 当文字时辅助平面字符时(`u+010000 - u+01FFFF`), codePointAt(0)可以返回实际的unicode值, 而charCodeAt(0)返回了前两个字节的十六进制值, 没有实际意义(代表一个空字符串)
// 下标是1的时候返回的都是后两个字节的十六进制值, 没有实际意义(页代表空字符串)
// 𠮷
'𠮷'.codePointAt(0).toString(16) // 20bb7 (\u+20bb7)
'𠮷'.codePointAt(1).toString(16) // dfb7 (\u+dfb7)
// 吉
'吉'.codePointAt(0).toString(16) // 5409 (\u+5409)
// 对比charCodeAt()方法
// 𠮷
'𠮷'.charCodeAt(0).toString(16) // d842 (\u+d842)
'𠮷'.charCodeAt(1).toString(16) // dfb7 (\u+dfb7)
// 吉
'吉'.charCodeAt(0).toString(16) // 5409 (\u+5409)
正则表达式中对2字节和4字节的处理
由于在js中, 𠮷字被当作两个空字符, 所以普通的正则表达式是查询不到这个字的!
/^.$/.test('𠮷') // false
/^.$/u.test('𠮷') // true
unicode正规化 (带有附加符号的单个字符)
//'Ǒ'
'\u01D1' // 'Ǒ' 一个码点表示一个字符
'\u004F\u030C' // 'Ǒ' 将附加符号单独作为一个码点,与主体字符复合显示,即两个码点表示一个字符
// 但是在js中
'\u01D1'==='\u004F\u030C' // false
// 需要使用normalize方法
'\u01D1'.normalize() === '\u004F\u030C'.normalize() // true
// 你
'\u4f60' // 你
'\u2F804' // 你
'\u{2f804}' === '\u4f60' // false
'\u{2f804}'.normalize() === '\u4f60'.normalize() // true
js中的encode,decode,escape对比
const s = `abcABC123,./<>?;':"[]\{}|=-+_)(*&^%$#@!~ 吉𠮷`;
encodeURI(s)
encodeURIComponent(s)
escape(s)
| 方法 | 结果 | 对这些不转义 | 其他 |
| encodeURI(s) | abcABC123,./%3C%3E?;':%22%5B%5D%7B%7D%7C=-+_)(*&%5E%25$#@!~%20%E5%90%89%F0%A0%AE%B7 | A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) # | encodeURI 自身无法产生能适用于HTTP GET 或 POST 请求的URI,例如对于 XMLHTTPRequests, 因为 "&", "+", 和 "=" 不会被编码,然而在 GET 和 POST 请求中它们是特殊字符。但是如果是用作浏览器请求地址,那还是应该用这个 |
| encodeURIComponent(s) | abcABC123%2C.%2F%3C%3E%3F%3B'%3A%22%5B%5D%7B%7D%7C%3D-%2B_)(*%26%5E%25%24%23%40!~%20%E5%90%89%F0%A0%AE%B7 | A-Z a-z 0-9 - _ . ! ~ * ' ( ) | 产生能适用于HTTP GET 或 POST 请求的URI,如果要用作浏览器请求地址就不行了,因为:/也会被编码 |
| escape(s) | abcABC123%2C./%3C%3E%3F%3B%27%3A%22%5B%5D%7B%7D%7C%3D-+_%29%28*%26%5E%25%24%23@%21%7E%20%u5409%uD842%uDFB7 | A-Z a-z 0-9 @*_+-./ | 已废弃,且不适用于url编码,只适用于字符串编 |
参考文章
两篇文章看懂unicode utf-8 utf16 之间的关系
www.ruanyifeng.com/blog/2007/1…
www.ruanyifeng.com/blog/2014/1…
一个有趣的emoji
👨👩👧👦 这个字符的length 是11
[...👨👩👧👦] // ["👨", "", "👩", "", "👧", "", "👦"]
emoji表情是一个普通的代表emoji的码点加上一些控制emoji的码点(例如U+FE0E,U+FE0F)显示的
可参考 segmentfault.com/a/119000000…
其他字符集
ASCII
一共128个字符,主要是英文字母,数字和英文标点
ISO-8859-1
单字节字符集,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
ANSI
ANSI字符集表示英文字符时用一个字节,表示中文或其他语言的字符时用 0x80~0xFFFF 范围的两个字节。
由于各不同语言都使用的是同一段字节范围,不同编码方式的 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
ANSI并不是某一种特定的字符编码,而是在不同的系统中,ANSI表示不同的编码。
在简体中文Windows操作系统中,ANSI 编码其实是 GB2312编码;在繁体中文Windows操作系统中,ANSI编码其实是Big5;在日文Windows操作系统中,ANSI 编码其实是 JIS 编码。在英文操作系统中,ANSI编码其实是ASCII编码
可以通过修改系统区域和语言,把文档显示出不同成不同的ANSI编码
GB2312
是中国发布的汉字编码字符集。主要收录了6763个汉字、682个符号。GB2312覆盖了汉字的大部分使用率,但不能处理像古汉语等特殊的罕用字
GBK
扩展了GB2312,在它的基础上又加了更多的汉字,向下兼容GB2312编码的,它一共收录了20000多个汉字。
GB18030
再一次扩展,收录了更多少数民族文字,现在PC一般都支持GB18030,但是手机等一般支持不到这么多。