前言
最近在做表单需求的时候遇到了emoji的问题,发现自己对字符编码上的理解还是过于浅显,于是翻阅资料,弥补缺陷。
What is unicode
简单来讲,
unicode
就是一个机器语音与人类语言的一张对应表。
可以理解为一个很大很大的
map
,唯一的
key
对应唯一的字符。
- 重要理念
-
码点
码点指的是
这个unicode
里的每一个map
key
。
例如
对应的是\u004a
这个字符,那么J
的码点就是J
74
-
unicode与编码
编码是对码点进行的编排,我们日常说的UTF-8, UTF-16, UTF-32都是编码方式。不同的编排方式有不同的优势与作用。
- 码点范围
-
基础平面
码点从0x0000 - 0xFFFF,共可存放65536个字符
-
辅助平面
码点从0x010000 - 0x10FFFF,共可存放16 * 65536个字符,可以划分为16个不同的平面
-
-
故我们可以认为目前unicode码表的码点最多可以用4个字节来表示
-
history
Ascii码大家都了解,128个字符,1个字节完全可以一一对应。但是计算机不是光美国使用,全世界人民都想用计算机,都想在计算机上显示出自己的语言。于是就对这1个字节的剩余128个空间开始搞事情。
那么问题来了,中国文化博大精深,128个空间对于中国文字来讲,简直不够塞牙缝。于是国人自己搞了一套GBK码表用两个字节就能表示65536个字符了,暂时解决了一下问题。
但是码表与编码不统一,必然会导致两个计算机解码显示上的差异,也就造成了所谓的乱码。
于是unicode的目的,就是为了建立一张能容纳全人类各种文化的字符的码表,来统一互联网的字符规范。
unicode 编码规则
通过编码规则可以对字符进行传输,通过解码后的码点与unicode表对照可以正确的显示文字
-
UTF-32
一种很简单的定长编码方式,均为4个字节编码
-
优点
查找复杂度为O(1)
-
缺点
浪费空间,传输内容只有ASCII时会是其的4倍。
-
故在网络传输中,传输内容的大小决定了流量与客户体验,这种编码方式并不适合互联网。
-
UTF-16
一种变长的编码方式,采用2个字节来表示基础平面,4个字节来表示辅助平面
编码范围 字节 0x0000 - 0xFFFF 2 0x010000 - 0x10FFFF 4 -
question
那么在编码与解码过程中,是如何区分应该是2字节还是4字节呢?
-
answer
我们知道,辅助平面的字符共有 16 * 65536个,也就是 2^20个。
在基本平面内,码点从0xD800到0xDFFF是一个空段,不对应任何字符。那么正好从0xD800-0xDBFF空间大小为1024(2^10),从0xDBFF-0xDFFF空间大小为1024(2^10)。于是就可以把辅助平面的码点分为高低位映射在这两个码点范围内,正好可以存储所有的辅助平面字符。
也就是说只要超过0xFFFF的码点,都会被编码成高位在0xD800-0xDBFF,低位在0xDC00-0xDFFF的4字节码点。这样解码的时候还是依次读取2字节,遇到0xD800-0xDBFF就知道这是个辅助平面字符,再读取2字节去解码
- 转码公式
- 如果是基本平面字符,直接将码点转为对应的16进制形式,长度为2字节
- 如果是辅助平面字符,则使用转码公式
H = Math.floor((c-0x10000) / 0x400)+0xD800 L = (c - 0x10000) % 0x400 + 0xDC00
-
-
UTF-8
一种变长的编码方式,越常用的字符编码长度越短
-
question
如何区分到底应该读取几个字节来编码解码呢?
- answer 编码规则:
编码范围 编码方式 字节 0x0000 - 0x007F 0xxxxxxx 1 0x0080 - 0x07FF 110xxxxx 10xxxxxx 2 0x0800 - 0xFFFF 1110xxxx 10xxxxxx 10xxxxxx 3 0x010000 - 0x10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 4 - 对于码点在0-127的,用1字节,第一位置0,后面按码点对应,其实就与ASCII相同了
- 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
-
由于UTF-8编码这种变长的编码规定,极大地节省了网络传输数据的空间,传输常用的ASCII码不用占据额外的空间,提升了效率
JavaScript编码规则
有趣的是,js与采用的是
UCS-2
编码方法。
UCS-2
是
UTF-16
编码的子集,只支持2字节解析,从而就导致了4字节字符被当做2个2字节字符解析,字符函数等就会出现问题。
- question
// '?'码点是0x1d306,在辅助平面内,按照UTF-16编码规则,会解析为高低位的2个2字节字符 '?'.length // 2 '?'.charCodeAt(0) // 55348 0xd834 '?'.charCodeAt(1) // 57094 0xdf06 '\ud834\udf06' === '?' // true '\u1d306' // ᴰ6 // 实际上只解析了2字节,把6当做了字符6,故不能正常按unicode直接解析出正确的字符 '\u1D306' === '\u1D30' + '6' // true // 那么自然 调用split等字符串处理函数必然会出现问题 '123?'.split('').reverse().join('') // 非 ?321 而是 ��321
-
answer
ES6为支持4字节字符提供了更多的解决方案
如果不支持ES6的话,只能老老实实做一些判断喽。for of 可以完全正确地遍历字符串 Array.from(str).length 可以得到正确的字符数 '?' === '\u{1d306}' // true 通过码点可以直接使用unicode表示辅助平面字符了 String.fromCodePoint(0x1d306) // 通过码点得到字符 '?'.codePointAt(0) // 119558 直接获得正确的码点 /^.$/u.test('?') // true 可以正常当做1个字符来检测了
有趣的unicode
-
emoji
通过上面的知识,不难猜测emoji也就只是unicode某个辅助平面里码点对应的一个字符而已
不过发送到后端存储的时候如果数据库是utf8编码的话会有一些问题,需要后端辅助设置一下数据库编码。不然的话我们只能想办法替换一下,然后接收数据后再替换回来 还是挺麻烦的。'?'.codePointAt(0) // 128522 '?'.length // 2 '\ud83d\ude0a' === '?' // true
- 颜文字
'(ง •̀_•́)ง'.length // 10 // 看上去只有8个字符,而length却是10,循环输出一下,发现问题在'•̀'上面 '•̀'.length // 2 '\u2022\u0300' === '•̀' // \u0300 是声调类符号,可以显示在前一个字符的某些位置上 // 我觉得这个表情还不够能给人鼓励。。于是。。 "\u0028\u0e07\u0020\u2022\u0300\u0300\u0300\u005f\u2022\u0301\u0301\u0301\u0029\u0e07\u0301\u0301\u0301" === "(ง •̀̀̀_•́́́)ง́́́" // 哈哈 是不是还蛮有意思的 // 有创造力的可以自行给自己的产品创建属于自己的颜文字了哈哈哈哈哈哈
参考
- http://www.ruanyifeng.com/blog/2014/12/unicode.html
- http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
- https://www.unicode.org/charts/
后记
了解unicode加深了我对字符串的理解和对乱码原因的理解,对日常开发还是狠狠狠有帮助的,尤其是对一些需要emoji的表单就要从开发时候做一些特殊的处理啦。
如果喜欢可以star一下,会不断更新github地址