详解Unicode和JavaScript字符编码

Unicode

Unicode,又称万国码、统一码和国际码,是由统一码联盟制定的一套规范统一的字符编码集,其设计意图是将世界上所有字符都包含在其中,它使用特定的十六进制编号来表示字符,每一个特定十六进制编号统称为码点,也叫码位,用“U+”紧接四位或五位十六进制数字来进行表示(例如U+597D表示中文“好”),在Unicode3.0中,也可用“U-”紧接八位十六进制数字表示。目前,Unicode整理并编码了世界上绝大部分的字符,并且已经普遍运用到各个编程语言,其中就包括了接下来要讲的JavaScript编码。

Unicode的最新版本是2020年3月发布的13.0,其中共收录了143,924个符号。当前实际应用的统一码版本是使用16位二进制的编码空间,也就是每个字符占用两个字节,理论上最多可表示216(即65536)个字符,其范围是0 ~216-1,写成十六进制就是U+0000 ~ U+FFFF,这基本满足当前各语言的日常使用,要注意的是16位编码空间也并未完全用于字符编码,其中留有相当一部分作为特殊作用或者后续扩展,例如后面要说的 “ 辅助区间 ”,上述16位编码空间被称为 “ 基本平面 ”,但最新版本扩展定义了其余16个“辅助平面”,其码点范围是U+010000 ~ U+10FFFF,用于收录更多的字符,每个平面可编码字符数量理论上与基本平面一致,所以17个平面至少需要占据21位编码空间,可收录字符数量理论上为221个,略小于三个字节,但实际上却是使用四个字节来存储辅助平面的字符,其目的主要是便于未来版本的扩充以及和其它字符集进行融合。

上述为Unicode做了个详细介绍,但是本质上只说明了Unicode的编码方式,而在实际传输或者存储的过程中,可能由于系统或者平台的差异或者出于空间节省的目的,在实现的方式上有所不同。简而言之

Unicode就是一本用指定一组特定二进制序列表示字符的字符典籍,它只制定字符的对应二进制序列,但没说明该字符二进制序列要以何种方式进行传输或存储。

Unicoded的实现方式就是常讲的Unicode转换格式(Unicode Transformation Format,简称为UTF),常见的Unicode转换格式有UTF-8UTF-16UTF-32,当然还有其它,这里就暂不讲述,以下各自讲一下这三种转换格式的特点和转码方式。

UTF-8

UTF-8,英语全称是8-bit Unicode Transformation Format,是一种针对Unicode的可变长字符编码,也是当前使用率最高和应用最广泛的转码方式。因为Unicode较小码点的使用频率较高,如果直接使用Unicode编码则会效率低下,并浪费大量内存空间,而本身UTF-8就是为了解决向后兼容ASCII编码而设计的,Unicode的前128个字符的二进制编码与ASCII码一一对应,所以为了节省空间,提高编码效率,UTF-8根据自己制定的编码规则,使用一至四个字节对Unicode所有的有效码点进行编码。

关于Unicode转换为UTF-8的编码规则其实很好理解,只有两条

  • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

  • 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母x表示可用编码的二进制位

Unicode符号范围(十六进制)UTF-8编码方式(二进制)
0000 0000-0000 007F0xxxxxxx
0000 0080-0000 07FF110xxxxx 10xxxxxx
0000 0800-0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。

下面以汉字 “ 严 ” 为例,演示如何实现 UTF-8 编码。

严的 Unicode 是U+4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是0xE4B8A5。

同样,解码UTF-8二进制序列的过程也很简单。当拿到一串经UTF-8编码后的字节序列时,对于其中的任意字节,如果第一位是0,则表示该字节是单字节字符(ASCII码);如果第一位是1,第二位是0,则表示该字节是多字节字符中的一个字节(非ASCII码);如果前两位是1,第三位是0,则表示该字节是两个字节表示的字符中的第一个字节,则和与其紧接的下一个字节组成该字符的字节编码。后面三字节和四字节表示的字符的识别规则同理。

我们还是以汉字 “ 严 ” 为例,简单实现UTF-8解码。

首先,我们收到一串二进制序列11100100 10111000 10100101,也知道它的转换格式是UTF-8,开始识别第一个字节可以知道后面两个字节都是某个字符的组成字节,根据上面表格,第一个字节前四位,后两字节前两位都是标识位,所以不提取,只提取非标识位,得到二进制序列100 111000 100101,其十六进制为0x4E25,对照Unicode码点表,与之相同码点对应的字符正好是汉字“严”。

至此,UTF-8的介绍就到这里。

UTF-16

UTF-16,全称16 bits Unicode/UCS Transformation Format,是Unicode字符编码五层次模型的第三层,也就是字符编码表(Character Encoding Form,简称CEF,也称为"storage format")的一种实现方式,它将Unicode字符集的抽象码位映射为16位长的整数(即码元,指有限大小的数字,此处UTF-16的16指的就是码元为16位)的序列,简单的说就是将字符码点用一个或多个码元表示。而对于Unicode字符集来说,一个码元表示所有字符是远远不够的,所以超出一个码元表示范围的字符都是用两个码元组成来表示的,所以实质上UTF-16也是一种变长字符编码。这和UTF-8有很大的不同,UTF-16的最小表示单位是两字节,而UTF-8是一个字节,所以UTF-16是无法兼容ASCII。

上面说过,码点范围U+0000 ~ U+FFFF是Unicode的基本平面,并且在这个区间内并不是每个码点都映射字符,而是留有一部分区间另有它用。Unicode标准规定,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符,而这个区间则用于对辅助平面的字符的码位进行编码,这段区间叫做代理区间。

 

我们先简单说一下UTF-16的编码规则,基本平面和辅助平面的编码方式是不一样的。

  • U+0000 ~ U+FFFF码位区间

Unicode第一个平面,也就是基本平面,恰好是一个码元空间,所以在该区间中的字符编码成UTF-16时,其UTF-16编码就是该字符的Unicode码点编码。

  • U+010000 ~ U+10FFFF码位区间

Unicode辅助平面在UTF-16中被编码成一对16比特长的码元(32位,4字节),其被称为“代理对”。具体编码规则如下

辅助平面的十六进制区间范围为0x010000 ~ 0x10FFFF,分别减去0x10000,得到值的范围是0 ~ 0xFFFFF,所以可以知道辅助平面的码位有220(1,048,576)个,而要完整表示该区间每个值需要20个二进制位。

UTF-16将这20位拆成两半,前10位加上0xD800,映射在0xD800~0xDBFF(空间大小2^10),称为高位代理(high surrogate);后10位加上0xDC00,映射在0xDC00~0xDFFF(空间大小2^10),称为低位代理(low surrogate),但是实质上高位代理的值要小于低位代理的值,所以为了避免混淆,高位代理也称为前导代理,低位代理也称为后尾代理。

此后意味着,一个辅助平面的字符,可以被拆成两个基本平面的字符表示,并进行相应的传输和存储。

这里有个地方要了解一下。如果我们用两个16位长码元序列来表示辅助平面中的220个字符,则第一个码元(即前导代理)要容纳上述20位中的前10位,第二个码元(即后尾代理)要容纳上述20位中的后10位,可以知道前导代理和后尾代理所能表示的个数都为210,也就是1024,故需要在基本平面中保留2048个不映射任何Unicode字符的码位才能符合需求,这也刚好是U+D800~U+DFFF区间的大小。

补充一个码元所表示字符是否在基本平面的判定规则。当接收到一连串经UTF-16编码后的字节序列时,会先读取第一个码元,也就是前16位,假如不在代理区间内,那说明该码元对应的字符码位在基本平面内。假如第一个码元在代理区间内,则可以断定该字符码位在辅助平面中,接下来则判断该码元的值是在高位代理的值范围还是在低位代理的值范围之内,如果在高位代理的值范围之内,则表示该码元是表示某字符的码元序列中的前一个码元,则与之紧接的下一个码元也是表示该字符的码元序列中的后一个。

下面以码位U+10437(𐐷)来演示一下UTF-16的编码过程

  • 0x10437 减去 0x10000,结果为0x00437,二进制为 0000 0000 0100 0011 0111;

  • 分割它的上10位值和下10位值(使用二进制):0000 0000 01 和 00 0011 0111;

  • 添加 0xD800(二进制11011000 00000000)到上值,以形成高位:0xD800 + 0x0001 = 0xD801;

  • 添加 0xDC00(二进制11011100 00000000)到下值,以形成低位:0xDC00 + 0x0037 = 0xDC37;

所以此时可以得到字符“ 𐐷 ” 的UTF-16编码 0xD801 0xDC37。

 

UTF-16的解码是编码的逆向过程,这里就不一一讲述。

UTF-32

UTF-32是最直观的编码方法,每个码点使用四个字节表示,字节内容一一对应码点。例如:

U+0000 = 0x0000 0000
U+597D = 0x0000 597D

UTF-32的优点在于,转换规则简单直观,查找效率高。缺点在于浪费空间,同样内容的英语文本,它会比ASCII编码大四倍。这个缺点很致命,导致实际上没有人使用这种编码方法,HTML 5标准就明文规定,网页不得编码成UTF-32。

以上是Unicode的主要内容,接下来讲在JavaScript字符编码与Unicode的关系。

 

JavaScript字符编码

JavaScript语言采用Unicode字符集,但是只支持一种编码方法,这种编码方法既不是UTF-16,也不是UTF-8,更不是UTF-32,而是UCS-2编码。UCS-2是使用两字节来表示一个字符的编码方式,使得JavaScript所有字符都是两个字节的大小,这也就是说UCS-2只能表示基本平面内的字符,其他辅助平面内的字符JavaScript无法正确识别,例如四字节的字符会被认作两个字符。

那为什么JavaScript不采用表示范围更加广的UTF-16呢?

  • 1984年国际标准化组织ISO创立了UCS项目。
  • 1988年由统一码联盟创立了Unicode项目。
  • 1990年ISO公布了第一套编码方法UCS-2。
  • 1991年前后,双方团队都认识到世界上不需要两个互不兼容的字符集,将两个字符集进行合并,使得所有字符都对应同一个码点。
  • 1995年5月Brendan Eich设计了JavaScript语言,并在同年10月第一个JS引擎问世。
  • 1996年7月Unicode发布了UTF-16编码方法。

从上面可以看到,在JavaScript语言创立之前,UTF-16还没被正式发布,所以只有UCS-2编码可以作为选择。

在2015年6月发布的ES6中,大幅增强了JavaScript对Unicode的支持,使得能够正确识别四字节表示的辅助平面字符,针对辅助平面的字符,JavaScript内部的识别规则和UTF-16一致,所以能很好地解决上述出现的问题。

实质上,UTF-16可以看成是UCS-2的父集,在没引入辅助平面前,UTF-16和UCS-2可以说是一致的,但在引入辅助平面后,UTF-16采用四字节表示辅助平面字符,使得表示范围大幅增加。

ok,对ES6的扩展内容,留于后续文章更新,对Unicode和JavaScript字符编码的介绍就先到这。

以下是我的公众号,专门推送前端的一些总结以及其它知识,有兴趣的关注一下呗。

“菜鸟札记”公众号

总字数:5374

完成时间:2021年3月2日 凌晨1:01

文章引用:Unicode与JavaScript详解Unicode维基百科UCS维基百科

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值