Unicode 和JS中的字符串

  计算机内部使用的是二进制,只认识0和1两个数字,它压根不知道文字是啥。但我们平时沟通交流使用的却是文字,而不是0,1数字。怎样才能让计算机认识文字? 只能先把文字存储成数字,然后再把数字转化成文字进行显示,这就相当于编码和解码,但编码和解码需要规则,到底用哪一个数字代表哪一个文字呢?

  如果在我的计算机上,1 表示A,2 表示B,3 表示C, 依次类推。而在你的计算机上,0表示A,1表示B。HELLO存储到我的计算机中就是数字8, 5, 12, 12, 15,我发HELLO给你,实际就是这些数字通过网络就发送给你了,当你的计算机接受到这些数字并解析时,8表示的却是I, 最终你收到的是IFMMP. 不同的计算机之间没有办法进行沟通。为了有效沟通,我们需要制定一个标准来编码文字,并且所有计算机都要遵守这个标准。

  19世纪60年代, 美国标准委员会制定了ASCII码,使用7个bit(7-bit)来编码一个文字. 7个bit(从0000000 to 1111111)能够表示128个值(0-127),所以ASCII码有足够的空间来表示大写字母,小写字母,数字等。在1968年,美国所有的计算机都使用ASCII码。当我再发送HELLO 给你的时候,HELLO 的编码变成72, 69,76,76, 79 ,在计算机内部使用二进制表示就是1001000,10000101,1001100,1001100,1001111。这些数字到达你的计算机,由于也是使用ASCII 码进行解析,所以能够正常显示为HELLO

  19世纪70年代,计算机普及到了欧洲,计算机的硬件也得到发展,CPU能一次处理8个bit,计算机的最小存储单位也变成了8bit(一个字节)。8个bit能表示256个值(0-255), 而ASCII码最多使用到127, 还剩下128-255 是没有用到。最开始的时候,IBM就用128-255 这些数字表示一些有符号的字字母和希腊字母,比如224表示希腊字母α。然而不像ASCII码,128-255能表示什么字符从来没有被标准化。欧洲各个国家都想使用这些没有用到的数字来表示自己的文字,并且这种思想同时出现,于是出现了个各种各样的字符集。比如俄罗斯人用244表示俄语 Я, 然而希腊人却用它来用表示小写的欧米茄,ω. 计算机之间的交流又陷入了混乱。

  19世纪90年代末,做了一次标准化的尝试和努力,创造出了15种不同的的字符集,它们都使用8个bit表示一个字符,叫做ISO-8859-1, ISO-8859-2, 直到 ISO-8859-16,中间的ISO-8859-12被废弃了。俄罗斯人使用的是ISO-8859-5, 224 表示字符 p,207 代表Я。 如果俄罗斯朋友给你发送了一封邮件或一份文档,最好知道他使用的是哪一种字符集。我们接受的邮件或文档是序例化的数字,数字224代表可能代表a, p 或 Я。如果我们使用了错误的字符编码打开文档,看到就是乱码。

  信息可以在不同的计算机上交流,但是你要确定使用的是哪一个字符编码。但随着计算机的进一步普及,它来到了亚洲,中日韩有大量的文字,8个bit, 256个值根本放不下,于是中国创造了GBK, 使用两个字节进行表示。国际化的到了,国与国之间沟通更为频繁,使得这种知道编码的交流方式愈发麻烦。

  Unicode诞生了。它的想法非常简单,就是给世界上每一个国家的每一个文字都分配一个独一无二的数字,这个数字称之为码点(code point), 前127个码点和ASCII一样,128-255基本上和ISO-8859-1一致,256之后就是其它有符号的字符,880 之后是希腊字母,中国,日本和韩国字符从11904开始。这样就不会产生混乱了,不管是哪个国家,哪种字符,都有一个唯一数字进行对应。俄罗斯的Я 永远都是1071.希腊字母阿尔法α永远都给是945。不过官方的unicode码点是用16进制进行表示,前面加上U+(表示unicode), H的unicode码点通常写成U+0048,而不是10进制的72。字符串Hello, 在unicode中对应5个码点:U+0048  U+0065  U+006C  U+006C  U+006F,但他们仅仅是数字,并没有说怎么存储到计算机中。Unicode只是提供了一个设想,并没有提供具体的实现,Unicode 最大的问题是它包含了太多的字符,大大超出了256,8个bits已经放不下了。

  最初(19世纪90年代)的思想是使用两个字节存储一个码点,因为那时各国的文字都比较少,只有7,161 个。在计算机中,两个字节(16个bit)就可以表示6万多个字,这就是UCS-2编码, Hello 变成了00 48 00 65 00 6C 00 6C 00 6F。但有一个比较尴尬的事情是,由于一个字符占用两个字节,对欧美人来说,有点浪费存储空间,他们也不愿意转换成unicode。有很长一段时间,他们忽略了unocide.

  但是随着时间的推移,尤其是亚洲国家把大量的象形文字也加入到Unicode 编码中,65536的数量也放不下这些文字,于是Unicode 编码字符集也相应的进行了扩充,它把整个字符集分成了17个平面,每一个平面都能放2^16 个文字, 每一个平面的码点从U+n0000 到U+nFFFF结束。n是用0x0 到0x10, 16进制表示

Unicode 和JS中的字符串

 

  第一个平面Plane 0放置的就是最初的UCS-2编码中所定义的文字(最常用的文字),叫做基本多文本平面Basic Multilingual Plane (or BMP),基本多文本平面码点的取值范围是0x0000 - 0xFFFF, 其他的16个平面称为补充平面(Supplementary Planes or Astral Planes), 它们的取值范围从0x10000 开始 (0xFFFF + 1 就等于0x10000,  16进制的计算)。针对这种多平面的字符集,UCS-2编码方式肯定是不合适了。最简单的方式,使用更多的字节来表示一个文字。此时计算机也得到了发展,CPU都是32bit 或64-bit, 一次能处理32个字节或64个字节。那为什么不能用更多的bit来表示一个字符? 当然可以,UTF-16, UTF-32 出现了,取代了UCS-2

  UTF-16 就是对于在BMP 平面的文字用两个字节(16个bit)进行表示,然后对于其他平面的文字用四个字节进行表示,

  UTF-32 则是用32个bit, 4个字节来表示一个字符,所有的字符都占用4个字节。

  使用UTF-16或UTF-32有什么问题呢?文件转输。如果你写的都是英文字符,它使用一个字节就可以了,但如果使用UTF-16,那就要占两个字节,UTF-32 就要使用4个字节,浪费带宽。最终UTF-8 出现,才解决这个问题。UTF-8 使用的是变长字节。码点从0-127 使用一个字节进行存储,只有码点大于128,才会用2个,3个,最大到6个字节进行存储。使用ASCII码写的文档,也是有效的UTF-8文档。 Hello的码点是 U+0048 U+0065 U+006C U+006C U+006F,   存储码计算机中 48  65  6C  6C 6F, 和 ASCII 一样。

  你可能还记得,写JS代码的时候要把文档格式改为UTF-8, HTML 文档也要设置成meta 标签也要设置成utf-8, 

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

  就是因为utf-8可以减少文档大小,不至于造成带宽的浪费。浏览网页,实际上是向服务器发送html, js 等静态资源的请求,这些资源肯定是越小越好。当浏览器请求到这些资源的时候,再对这个资源进行解析,解析的依据就是charset 设定的字符编码。我们的文件是用utf-8 写的,浏览器进行解析的时候,也用utf-8,所以它能正确的渲染页面。这也要求设置charset的meta标签尽可能早的出现在<head> 标签中。浏览器在解析html文档的时候,它会先按操作系统默认字符编码进行解析,如果它碰到<meta http-equiv="Content-Type" content="text/html; charset=utf-8">,它会停止解析,再按charset 设置的utf-8 重新进行解析。

  书写的JS代码的时候是使用UTF-8, 但JavaScript 使用的是UTF-16,这怎么理解?JS引擎在内部做了一次UTF-8到UTF-16的转换。当浏览器解析完js代码,js引擎执行js代码的时候,如果它碰到字符串,它就会把它转成换UTF-16. JavaScript 的源代码文件也可以是任意格式,但源代码中的字符串一定是UTF-16. While a  javascript  source file  can  have  any  kind  of  encoding,  JavaScript  will  then  covert it  internally  to  UTF-16  before  executing  it.  JavaScript  strings  are  all  UTF-16    sequences,  as  the  ECMAScript  standard  says:

  When a String contains actual textual data, each element is considered to be a single UTF-16 code unit

  码元(code unit) 就是指字符在计算机中所占的物理存储空间,占了多大内存。对于UTF-16来说,BMP 平面中的文字可以用一个16个bit进行存储,BMP平面最多存放65536个字符,16个bit完全可以搞定,16个bit 就称之为码元。一个16bit 就是一个码元。其他平面的文字要用(2个16bit)进行存储,UTF-16是按16bit进行递增的。一个16bit 不行,那就再加一个16bit,这一个个的16bit 就是码元。2个16bit 就是2个码元。码点是unicode 字符集中的术语,它只是一个普通数字,没有涉及到任何计算机的存储,UTF-16 等编码,是指字符是怎么存储到计算机中的,一个字符,到底占用多少个bit,多大的内存空间。UTF-16, 以16bit 为基础,以16bit 进行递增,所以对于UTF-16来说,一个16bit 就是一个码元。对于unicode 字符集(码点)来说,如果以UTF-16 进行编码,它要么占一个16bit, 一个码元,要么2个16bit,2个码元。

  那怎么才能让一个码点用两个码元来显示呢? 先看一下目前Unicode 非基本面的有多少个字, 一共16个面, 一个面是2^16 个字,那也就是说,一共有16 * 2^16 个字, 就是2^20个字符, 也就需要20个二进制bit 位,一个码元只能16bit 位,那只能把20bit 位进行拆分成2个部分,一个部分占10个bit, 这样一个码元就可以表示了。但这又存在另外一个问题,当计算机去读取字符的时候,它碰到一个字节就去读取一个字节,它并不知道,你这个字节是拆分出来的,你会发现,本来一个字,确显示出了毫不相干的两个字,这怎么办呢?在BMP平面,它所定义的Unicode 字符(55444个)并没有完全使用了2个字节,有一部分是空的,并没有对应任何字符,这一段是0xD800 到 0xDFFF(55296 - 57343), 这也就意味着,使用这一段上的码元没有任何问题。那我们就想办法,让非基本面的码点对应到这一段上来。对应的逻辑是这样的,首先把0xD800 - 0xDFFF  分为两个部分,0xD800 到 0xDBFF 和0xDC00 到 0xDFFF,然后再用码点减去65536, 把剩下的数用二进制表示,如果二进制不够20位,可以在前面补0.  再把20位二进制分为两个部分,每个部分占用10个bit,  前10个bit 对应到0xD800 - 0xDFFF, 称为高位(H), 后10个bit 对应到0xDC00 到 0xDFFF 称为低位(L), 这样一个码点就可以用两个码元(高位, 低位)进行表示了。映射比较复杂,不过Unicode 提供了一个公式,可以直接通过码点得到高低位码元。

H = Math.floor((c-0x10000) / 0x400)+0xD800  // c 是码点 0x10000 就是65536  0x400 就是1024, 0xD800 就是H 开始位置

L = (c - 0x10000) % 0x400 + 0xDC00  // 0xDC00  L 位开始的位置。 

  举一个小例子,汉字"??"的码点为134071, 大于65535, 它要用4个字节(2个码元)进行表示。把码点134071代入到上面的c 中, 由于浏览器的控制台可以直接计算16进制,我们把134071 转化成16进制 0x20BB7

Unicode 和JS中的字符串

  得到十进制55362, 转化成16进制为0xD842, 此为高位

Unicode 和JS中的字符串

  十进制57271,转化成16进制为0xDFB7, 此为低位。

  汉字"??"的 UTF-16 编码为0xD842 0xDFB7 ,称之为surrogate pairs(代理对),一对16bit 码元来表示一个码点。

  当使用这种方式用两个码元去表示一个码点时,计算机也不会出错,因为当它读取到第一个码元的时候,发现并没有对应的字符,它会就继续向下取第二个码元,两个码元连起来,就是对应的码点,就可以显示一个字了。

 

Unicode 和JS中的字符串

上一篇:c#开发移动APP-Xamarin入门


下一篇:httprunner 2.x学习8-参数化(引用 debugtalk 函数)