js地址中百分号字符转化为汉字_追本溯源:字符串及编码
开始
先考虑下边的问题。
我们知道 length 就是字符串的字符数,所以输出的依次是 2,1,1,对吗?
探索⼀
我们知道,计算机⾥只能存 0 和 1,换⾔之,只能存数字,⽽我们现在在屏幕上看到的⽂字只是将数字对应到图形⽽已。
unicode汉字早期的 ASCII 码就是典型的例⼦,如下图,为了书写⽅便我在数字前边加了 0x 代表是 16 进制。
我们⽤ 106 代表 ' j ',115 代表 ' s ' 。然后如果⽤ ASCII 码表⽰ " js " 的话,其实就是 0110101001110011 ,然后每 8 位也就是⼀个字节组成⼀个数字,根据对应关系电脑把本来的数字转换成了字符 “js” 展⽰到了我们⾯前。
有⼀个缺点就是 ASCII 码是 8 位,那么只能表⽰ 28" role="presentation" >2 的 8 次⽅个数字,也就是 256 个数字,这对于英⽂字母已经⾜够了。但是对于汉字的话,还远远不够。
探索⼆
所以我们加 1 个字节,⽤两个字节的数字去对应汉字,2 的 16 次⽅,也就是 65536,肯定⾜够了。
当然,每个国家都会这样想,然后都制定了⾃⼰的语⾔相应的对应规则,这当然不⽅便⼤家在互联⽹上互通有⽆,如果本机不知道对应国家的编码对应关系,从⽽会造成乱码。所以后来有了 Unicode。
我们⽤ 0x000000 - 0x10FFFF 这么多的数字去对应全世界所有的语⾔、公式、符号。然后把这些数字分成 17 部分,把常⽤的放到
0x0000 - 0xFFFF,也就是 2 个字节,叫做基本平⾯ (BMP)。从 0x010000 - 0x10FFFF 再划分为其他平⾯。
和 ASCII 码⼀样,我们可以把每个符号对应于⼀个数字,这个数字我们也把它叫做码点值。
有了对应关系,我们可以像 ASCII 码那样去存了。当然这⾥的话因为每个字符都对应 24 ⽐特位的数字,所以我们就⽤ 3 个字节去存它吧。但是考虑到 CPU 的寄存器都是 8 位,16 位,32 位。。。翻倍来的,所以即使⽤ 24 位,最终还得转到 32 位,所以我们直接⽤ 32位吧。
是的,这就是传说中的 UTF - 32 编码,简单明了,码点值是多少,内存中就存多少。
探索三
UTF - 32 缺点很明显了,字母 A 原本只需要 1 个字节去存储,⽽现在却⽤了 4 个字节去存,⼤部分位置都是 0。
我们为什么要多存那么多零呢?能不能 A 只存 0x41,亮只存 0x4eae。如果 A亮这个字符串放到内存中就是 0x414eae。问题来了,计算机怎么知道,⼏个字节代表⼀个字符呢?是 0x41呢?还是 0x414e 呢?还是 0x414eae?
于是,就有了 UTF - 8,将码点值进⾏⼀定的转换再去存储。
把阮⼀峰⽼师的讲解搬过来。
跟据上表,解读 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,转换成⼗六进制就是E4 B8 A5。
让我们再看下「亮」,码点值是0x4eae,⼆进制就是 100111010101110,同样符合第三⾏,即格式是1110xxxx 10xxxxxx 10xxxxxx。
从亮的最右边⼆进制位开始,依次从右往左填⼊上边格式中的x,多出的位补0。这样就得到了,亮的 UTF-8 编码是 1110(0100)
10(111010) 10(101110),16 进制就是 e4 ba ae。
所以现在的对应关系变成了下边的样⼦。
和 UTF - 32 不同之处在于,我们不再⽤ 4 个字节存储码点值,⽽是通过规则转换后再存储,这样的好处就是之前的A的话就只需 1 个字节就够了,⽽其他的可能是 2 个或 3 个,4 个字节,所以 UTF - 8 也叫变字长编码。
由于 UTF - 8 的变字长,⽽对于⼤部分常⽤字符都是 1 或 2 个字节,所以对于 html、邮件的传输多⽤ UTF-8 进⾏编码后传输。
探索四
UTF - 8 有什么缺点吗?
对于⼀个字符串abc天⽓不错,如果我们知道它的总共⼤⼩是 19 字节,但是我们很难算出它有多少个字符。因为有的字符是 1 个字节,有的是 2 个字节,有的是 3 个。所以为了知道字符数,我们还需要遍历⼀遍所有字节,从⽽确定有多少个字符。此外如果我们想取第 3 个字符,我们还是得从第 0 个字节开始遍历,因为我们不知道每个字符有多少字节。
如果每个字符都⽤固定长度编码就好了,这不⼜回到 UTF - 32 了吗?不不不,我们折中⼀下。
对于 Unicode 字符集,基本平⾯是我们常⽤的⼀些字符,⽤两个字节就可以编码。所以对于亮字的话,码点值是0x4eae,那么我们内部就⽤ 0x4eae 去存。⽽ ASCII 码只需要⼀个字节,那么我们把通过⾼位补零扩充⾄两个字节去存。例如A的码点值是 65,16 进制对应
0x41,⽤ U+41 表⽰。那么内部的话就⽤ 0x0041 去存。
那么基本⾯以外的字符呢?⽐如?(可能显⽰不出来)这个字就属于基本⾯以外,它的 Unicode 码点值是 178178,也就是 0x2b802 ,显然⽤两个字节是存不下的,那怎么办呢?
⽤四个字节存呗,像 UTF - 32 那样直接存码点值,然后⾼位补零吗?显然不⾏了,因为第⼀平⾯我们是⽤的两个字节,如果第⼀平⾯外的直接⽤四个字节去存码点值的话,可能会导致前两个字节和基本
⾯的两个字节重复,导致我们⽆法区分当前字符是两个字节还是四个字节。
UTF - 8 中,我们根据⼆进制开头的 1 的个数来表⽰当前字符是⼏个字节。这⾥的话,幸运的是在第⼀平⾯ U+D800 .. U+DFFF 的值不对应于任何字符。所以我们可以根据⼀些算法,把码点值转换为 4 个字节,前两个字节就⽤ U+D800 .. U+DFFF 中的值,这样如果前两个字节是 U+D800 .. U+DFFF 范围内的数,那就意味着该字符是 4 个字节编码的。否则就是两个字节。
这就是 UTF - 16 的编码⽅式了(具体的算法⼤家可以⽹上⼀下),相对于 UTF - 8 的优势就是固定字节数,⼤部分字符都是两个字节。所以如果对于⼀个字符串abc天⽓不错如果采⽤ UTF - 16 编码,我们知道了它的总⼤⼩是 14 字节,那么字符数就很好知道了,它的⼤⼩除以 2 就是它的字符数了。⽽取第 4 个字符,如果知道了字符串开头的地址,也只需要加 2 * 4 就可以了(下标从 0 开始)。对于字符串的切割合并也都很好操作了。
所以对于⼀些语⾔ java,javascript ⾥的字符串也都⽤了 UTF - 16 编码。所以回到最开始的问题。
那么就取决于这些字符是不是在第⼀平⾯内了,如果是的话,那么结果就会就是 2 1 1。遗憾的是 “?” 这个字并不在基本平⾯,所以它内部是⽤四个字节编码,⽽ js 为了⽅便简单,它简单粗暴的认为两个字节就是⼀个字符,所以输出的就是 2 了。
实验验证
接下来说⼀下⽂件的存储。
我们打开⼀个 .txt,看到很多⽂字、符号,⽽内部其实也是⽤ 0、1 存储的。既然要存储,就需要把 Unicode 的码点值进⾏编码。
如果是 UTF - 8 编码,那么⼀个码点值会⽣成 1 个或多个字节,然后把这些字节按顺序存就可以了。
如果是 UTF - 16 编码呢?
我们知道⼀个 Unicode 的码点值会对应⼀个数字,对于基本平⾯的字符,我们直接把这个数字存到内存中。那么问题来了,我们知道亮的码点值是 20142,换成 16 进制就是 0x4eae,内存中是按字节进⾏编址的。所以我们是先存4e呢?还是ae?先存4e吧,这样就符合我们⼈类阅读顺序,先读4e,所以先存4e呗。所以在内存中就是下边的样⼦。
内存地址内存值0x00000000 01001110 (4e)0x00000001 10101110 (ae)
那么问题⼜来了,计算机处理的话先读取的是低地址,也就是4e,⽽4e对应数字0x4eae的⾼位(如果是 10 进制,个⼗百千,千就叫做⾼位)。有时候我们希望从低位读(也就是⼗进制中的个位)数字,所以我们希望这样去存。
内存地址内存值0x00000000 01001110 (ae)0x00000001 10101110 (4e)
这就是多个字节存储的时候的字节序问题,把数字的⾼位存到低地址,低位存到⾼地址,叫做⼤端序(big endian),存储顺序符合我们⼈类习惯。反之就叫⼩端序(little endian)。
如果把亮字存到⼀个 .txt 中。
如果⽤ UTF-8 编码,那么前边算过的,就是e4 ba ae。
如果⽤ UTF-16 编码,⼤端序的话就是4eae。
如果⽤ UTF-16 编码,⼩端序的话就是ae4e。
我们可以验证⼀下,可以⽤ notepad++,安装⼀个 HEXEditor 插件即可。或者其他的可以查看内部编码的也⾏。
写⼀个亮到
以 UTF - 8 编码。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论