JavaScript与文字编码

2018-01-14 阅读 1144 收藏 0 原链:barryliu1995.studio
分享到:

前端必备图书《React快速上手开发 开源技术React Web应用》 >> >> 

在学习阮一峰的 字符串的扩展|ECMAScript 6 入门 章节的时候其中有一处“JavaScript 共有 6 种方法可以表示一个字符”。然后想起之前看的一篇关于文字编码的文章,所以现在打算重新复习一下文字编码

文字编码

在物理层面上,我们存入计算机的所有文件都是以二进制形式保存的。但现实生活中,我们会发现计算机中保存的文件形式各式各样,有音频文件、文本文件、视频文件、还有我们程序员熟悉的源代码文件等等。由此可以看出,不同文件的差别只是我们解读二进制的方式不同。然而即使同为一个类别的文件,解读二进制文件又会有不同。其中我们以网页开发中最常接触到的文本文件的编码方式拿来细说。这里所说的文本文件,一般是指基于字符编码的文件。计算机发展到现在为止,出现的字符编码就有 ASCII、GB2312、Unicode等等。

ASCII

使用 8 个二进制位进行编码的方案,最多可以给 256 个字符(包括字母、数字、标点符号、控制字符及其他符号)分配数值。基本的 ASCII 字符集共有 128 (0~127)个字符,其中有 96 个可打印字符,包括常用的字母、数字、标点符号等,另外还有 32 个控制字符。因为这 128 个基本字符就可以用 7 位二进制表示。所以多出来的最高位设为 0 (在数据传输时可用作奇偶校验位)。剩下的 128 个拿来扩充 ASCII 字符集,为了以后方便加入新的字符。这些扩充字符的编码最高为均为 1 的二进制数字(即十进制 128~255),称为扩展 ASCII 码。由于这些基本 ASCII 字符集字符数目有限,在实际应用中往往无法满足要求。国际标准化组织(ISO)制定了新的标准,该标准是兼容了基本 ASCII 字符集,在不同国家地区有不同具体的方案。

Unicode

为了解决不同国家地区间编码方案的不同引起的不便,于是就有了这样一个想法:将全世界所有的字符包含在一个集合里,计算机只要支持这一个字符集,就能显示所有的字符,再也不会乱码了。

Unicode 就是在这种背景下诞生了。Unicode 规定了每个字符的编号,这个编号也叫做“码点”(code point)。它从 0 开始,目前 Unicode 的最新版本一共收入了 109449 个符号,其中的中日韩文字为 74500 个。这么多符号,Unicode 不是一次性定义的,而是分区定义。每个区可以放 65536 (216216)个字符,称为一个平面(plane)。目前,一共有 17 (2525)个平面,也就是说,整个 Unicode 字符集的大小现在是 221221 个。其中最前面的 65536 个字符位,称为基本平面(缩写BMP),它的码点范围是从 0 ~ 216−1216−1,写成十六进制就是从 U+0000 ~ U+FFFF 。所有最常见的字符都放在这个平面,这是 Unicode 最先定义和公布的一个平面。剩下的字符都放在辅助平面(缩写SMP),码点范围从 U+010000 ~ U+10FFFF。

Unicode 只规定了每个字符的码点,到底用什么样的字节序表示这个码点,就涉及到编码方法。

UTF-32

最直观的编码方法就是,每个码点使用 4 个字节表示,字节内容一一对应码点。这种编码方法就叫做 UTF-32。例如码点 597D

U+597D = 0x0000 597D

UTF-32

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

UTF-8

UTF-32 太过于浪费空间,所以急需一种节省空间的表示方案,这导致了 UTF-8 的诞生。UTF-8 是一种变长的编码方法,字符长度从 1 个字节到 4 个字节不等。越是常用的字符,字节越短,最前面的 128 个字符,只使用 1 个字节表示,与 ASCII 码完全相同。由于 UTF-8 这种节省空间的特性,导致它成为互联网上最常见的网页编码。

字节 编号范围
0x0000 - 0x007F 1
0x0080 - 0x07FF 2
0x0800 - 0xFFFF 3
0x010000 - 0x10FFFF 4

我们来看看 UTF-8 具体是如何存储字符的,如下面的字符

we 发 财 🤑

上面的字符对应的十进制编码如下

其中 20 是空格的编码,可以看到一个英文还是 1 个字节,一个中文用了 3 个字节,而一个 Emoj 用了 4 个字节。它怎么知道每次应该读取多少个字节呢?如下图所示:

如果一个字节是 0 开头,表示这个字节就表示一个字符,如果是 3 个 1 开头表示这个字符要占 3 个字节,有多少个 1 就表示当前字符占用了多少个字节。这个就是 UTF-8 的存储特点,UTF 规定了每个字符的编号,而 UTF-8 定义了字符应该怎么存储。从 Unicode官网 可以查到,“我”的UTF编码是6211,如下图所示:

6211 怎么变成 UTF-8 编码呢?因为 6211 落在下面这个范围:

U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX

所以是这么转的:

“我”的 UTF-8 就是 E6 88 91

UTF-16

UTF-16 编码介于 UTF-32 与 UTF-8 之间,同时结合了定长和变长两种编码方法的特点。它的编码规则很简单:基本平面的字符占用 2 个字节,辅助平面的字符占用 4 个字节。也就是说,UTF-16 的编码长度要么是 2 个字节(U+0000到U+FFFF),要么是 4 个字节(U+010000到U+10FFFF)。

但是现在面临一个问题就是:当我们遇到两个字节,怎么看出它本身是一个字符,还是需要跟其他两个字节放在一起解读?然而在基本平面内,从 U+D800 ~ U+DFFF 是一个空段,即这些码点不对应任何字符。因此,这个空段被拿来映射辅助平面的字符。当我们遇到两个字节,发现它的码点在 U+D800 ~ U+DBFF 之间,就可以断定,紧跟在后面的两个字节的码点,应该在U+DC00 ~ U+DFFF之间,这四个字节必须放在一起解读。

到此,我们可以看出,UTF-8 的优点在于 1 个英文只要 1 个字节,但是 1 个中文却是 3 个字节,UTF-16 的优点在于编码长度固定,1 个中文只要 2 个字节,但是 1 个英文也要 2 个字节。所以对于英文网页 UTF-8 编码更加有利,而对于中文网页使用 UTF-16 应该更加有利。因为绝大部分的中文都是落在 U+0000 ~ U+FFFF。

Base64

早期电子邮件的 SMTP(Simple Mail Transfer Protocol) 协议仅限于传送 7 位的 ASCII 码。许多其他非英语国家的文字以及多媒体资源就无法传送,所以说早期的电子邮件就只能传送英文文字。或许是早期电子邮件的诞生的时候,美国人民以为只有他们自己会传送文字以取消传统的电报业务。

于是后来就提出了通用互联网邮件扩充(MIME)。MIME 并没有改动或取代 SMTP。MIME 的意图是继续使用原来的邮件格式,但增加了邮件主体的结构,并定义了传送非 ASCII 码的编码规则。因此在这种情况下,Base64 就诞生了。

Base64 可以把原本 ASCII 码的控制字符甚至 ASCII 码之外的字符都转成可打印的 6 bit 字符,也就是说用 6 bit 字符表达了原本 8 bit 字符。我们看看下面的图片:

像表格中那样,8 Bit <mo>×</mo>" role=“presentation” style=“position: relative;”>× <mo>×</mo>3 的字符串可以每 6 个 bit 分成一组,每一组 bit 对应一个十进制的 index,每一个 index 值又对应了 Base64 的字符。由于 6 bit 的二进制代码共有 64 种不同的值,从 0 到 63。用 A 表示 0 ,用 B 表示 1 ,等等。26 个大写字母排列完毕后,接下去再排 26 个小写字母,再后面是 10 个数字,最后用“+”表示 62,而用“/”表示 63。再用两个连在一起的等号 “==” 和一个等号 “=” 分别表示最后一组的代码只有 8 bit 或 16 bit。

JavaScript 与编码

JavaScript使用哪种编码

JavaScript 语言采用 Unicode 字符集,但是它编码使用的既不是 UTF-16,也不是UTF-8,更不是 UTF-32,而是 UCS-2。他们之间是什么关系呢?我们讲讲一些历史。

互联网还没出现的年代,曾经有两个团队,不约而同想搞统一字符集。一个是 1988 年成立的Unicode 团队,另一个是 1989 年成立的 UCS 团队。等到他们发现了对方的存在,很快就达成一致:世界上不需要两套统一字符集。1991 年 10 月,两个团队决定合并字符集。也就是说,从今以后只发布一套字符集,就是 Unicode,并且修订此前发布的字符集,UCS 的码点将与 Unicode 完全一致。

UCS 的开发进度快于 Unicode,1990 年就公布了第一套编码方法 UCS-2,使用 2 个字节表示已经有码点的字符。(那个时候只有一个平面,就是基本平面,所以 2 个字节就够用了。)UTF-16 编码迟至 1996 年 7 月才公布,明确宣布是 UCS-2 的超集,即基本平面字符沿用 UCS-2 编码,辅助平面字符定义了 4 个字节的表示方法。

**两者的关系简单说,就是 UTF-16 取代了 UCS-2 ,或者说 UCS-2 整合进了 UTF-16。**所以,现在只有 UTF-16,没有 UCS-2。

###JavaScript为什么不使用 UTF-16

那么,为什么 JavaScript 不选择更高级的 UTF-16,而用了已经被淘汰的 UCS-2 呢?答案很简单:非不想也,是不能也。因为在 JavaScript 语言出现的时候,还没有 UTF-16 编码。

JavaScript 字符函数的局限

由于 JavaScript 只能处理 UCS-2 编码,造成所有字符在这门语言中都是 2 个字节,如果是 4 个字节的字符,会当作两个双字节的字符处理。JavaScript 的字符函数都受到这一点的影响,无法返回正确结果。所以当遍历字符串的时,必须对码点做一个判断,然后手动调整。

  • String.prototype.replace()
  • String.prototype.substring()
  • String.prototype.slice()

上面的字符函数都只对 2 字节的码点有效。要正确处理 4 字节的码点要手动处理。

ES6

JavaScript 的下一个版本 ECMAScript 6(ES6),大幅增强了 Unicode 支持,基本上解决了上面的问题。新增了以下特点:

  • ES6能够自动识别 4 字节的码点
  • 允许直接用码点表示 Unicode 字符
  • 新增了几个专门处理 4 字节码点的函数
  • 对正则表达式添加了 4 字节码点的支持
  • Unicode 正规化
回到顶部