1. Unicode背后的思想

首先问一个最基础的问题:你是怎样阅读并理解这篇文章的?答案很简单,因为你明白这些字以及由字组成的单词的含义。

那你又是如何明白这些字的含义的呢?答案也很简单,因为你(读者)和我(作者)对于这些(呈现在屏幕上的)图形与汉字(即含义)之间的联系有着相同的认知。

对计算机来说这个原理也差不多,只有一点不同:计算机不懂这些字(字母)的含义,只是将其理解为特定的比特序列。

让我们设想一个情景:计算机User1向计算机User2发送一条消息'hello'

计算机并不知道这些字母的含义。所以计算机User1将消息'hello'转换为一串数字序列0x68 0x65 0x6C 0x6C 0x6F,每个字母对应一个数字:h对应0x68, e对应0x65,等等。

接着将这些数字发送给计算机User2

计算机User2收到数字序列0x68 0x65 0x6C 0x6C 0x6F后,使用同一套字母与数字的对应关系重建消息内容,'hello'就能正确地显示出来了。

不同计算机之间对字母与数字之间对应关系的协议就是Unicode进行标准化的结果。

根据Unicode,h是一个名为LATIN SMALL LETTER H的抽象字符。这个抽象字符对应数字0x68,也就是一个标记为U+0068的代码点。这些概念将在下一章中说明。

Unicode的作用就是提供一个抽象字符列表(字符集),并给每一个字符分配一个独一无二的标识符代码点(编码字符集)。

2. Unicode基本概念

www.unicode.org网站提到:

Unicode为每一个字符分配一个专有的数字

不分平台

不分程序

不分语言

Unicode是一个世界通用的字符集,它定义了全世界大部分书写体系的字符集,并为每一个字符分配了一个独一无二的数字(代码点)。

Unicode logo
Unicode logo

Unicode囊括了大部分现代语言、标点符号、附加符号(变音符)、数学符号、技术符号、箭头和表情符号等。

Unicode第一版1.0于1991年10月发布,包含7161个字符。最新版9.0(2016年6月发布)则提供了128172个字符的编码。

Unicode的通用性与开放性解决了过去一直存在的一个问题:供应商们各自实现不同的字符集和编码规则,很难处理。

创建一个支持所有字符集和编码规则的应用是十分复杂的。更不用说你选用的编码可能不支持所有你需要的语言。

如果你觉得Unicode很难,那就想想如果没有它编程会更难。

我还记得从前随机选择所需的字符集和编码规则去读取文件内容的时候。全靠人品啊!

2.1 字符与代码点

抽象字符(即文本字符)是用来组织、管理或表现文本数据的信息单位。

Unicode中的字符是一个抽象概念。每一个抽象字符都有一个对应的名称,例如LATIN SMALL LETTER A。该抽象字符的图像表现形式(glyph)是a。(译者注:glyph即图像字符)

代码点是指被分配给某个抽象字符的数字

代码点以U+<hex>的形式表示,U+是代表Unicode的前缀,而<hex>是一个16进制数。例如U+0041U+2603都是代码点。

代码点的取值范围是从U+0000U+10FFFF

记住代码点就是一个简单的数字。思考有关Unicode的问题时要记得这一点。

代码点就好像数组元素的下标。

Unicode的神奇之处就在于将代码点与抽象字符关联起来。例如U+0041对应的抽象字符名为LATIN CAPITAL LETTER A (表现为A),而U+2603对应的抽象字符名为SNOWMAN(表现为)

注意,并非所有的代码点都有对应的抽象字符。可用的代码点有1114112个,但分配了抽象字符的只有128237个。

2.2 Unicode平面

平面是指从U+n0000U+nFFFF的区间,也就是65536(1000016)个连续的Unicode代码点,n的取值范围是从016到1016

这些平面将Unicode代码点分为17个大小相等的集合:

  • 平面0包含从U+0000U+FFFF的代码点

  • 平面1包含从U+1 0000U+1 FFFF的代码点

  • 平面16包含从U+10 0000U+10 FFFF的代码点

Unicode planes
Unicode planes

基本多文种平面

平面0比较特殊,被称为基本多文种平面或简称BMP。它包含了大多数现代语言的字符 (基本拉丁字母, 西里尔字母, 希腊字母等)和大量的符号

如上文所述,基本多文种平面的代码点取值范围是从U+0000U+FFFF,最多可以有4位16进制数字。

大多数时候开发者处理的都是BMP中的字符。它包含了大多数情况下的必需字符。

BMP中的一些字符:

  • e对应代码点U+0065 抽象字符名: LATIN SMALL LETTER E

  • |对应代码点U+007C 抽象字符名: VERTICAL BAR

  • 对应代码点U+25A0 抽象字符名: BLACK SQUARE

  • 对应代码点U+2602 抽象字符名: UMBRELLA

星光平面

BMP之后的16个平面(平面1,平面2,…,平面16)被称为星光平面辅助平面

星光平面的代码点被称为星光代码点。这些代码点的取值范围是从U+10000U+10FFFF

星光代码点可能会有5位或6位16进制数字:U+dddddU+dddddd

来看几个星光平面里的字符:

  • 𝄞对应U+1D11E抽象字符名:MUSICAL SYMBOL G CLEF

  • 𝐁对应U+1D401抽象字符名:MATHEMATICAL BOLD CAPITAL B

  • 🀵对应U+1F035抽象字符名:DOMINO TITLE HORIZONTAL-00-04

  • 😀对应U+1F600抽象字符名:GRINNING FACE

2.3 码元

计算机在存储时当然不会使用代码点或抽象字符,它们是存在于开发者大脑中的概念。

所以自然要有一种在物理层面表示Unicode代码点的方式:码元。

码元是指使用某种给定的编码规则给抽象字符编码后得到的比特序列。

字符编码将抽象层面的代码点转换为物理层面的比特序列:码元。

换句话说,字符编码的作用就是将Unicode代码点翻译成独一无二的码元序列。

常用的字符编码有UTF-8, UTF-16UTF-32.

大多数JavaScript引擎使用UTF-16编码字符。它会影响JavaScript处理Unicode的方式。所以从这里开始让我们集中精力于UTF-16吧。

UTF-16(全称:16位统一码转换格式)是一种变长编码:

  • BMP中的代码点编码为单个16位的码元
  • 星光平面的代码点编码为两个16位的码元

来看几个例子

假设我们想把LATIN SMALL LETTER A,也就是抽象字符a存入硬盘。Unicode告诉我们抽象字符LATIN SMALL LETTER A对应代码点U+0061

现在我们来看看UTF-16如何转换U+0061。编码规范上说,对于BMP中的代码点只需将它的16进制数字U+0061存入一个16位的码元就行了。

显然,BMP中的代码点刚好能存进一个16位的码元。编码BMP可谓小菜一碟。

2.4 代理对

现在让我们来研究一个复杂些的例子。假设我们想存储一个星光代码点(属于星光平面): GRINNING FACE character 😀。该字符对应的代码点是 U+1F600

由于星光代码点需要21个比特来存储字符信息,UTF-16需要两个码元来编码,每个16比特。代码点 U+1F600 被拆分为所谓的代理对:0xD83D(高位代理码元)与 0xDE00(低位代理码元)。

代理对用来表示那些对应2个16位码元序列的抽象字符,其中第一个码元是高位代理码元而第二个是低位代理码元

编码一个星光代码点需要两个码元:即一个代理对。比如前面那个例子,使用UTF-16编码U+1F600 (😀)就使用了一个代理对:0xD83D 0xDE00

Try in repl.it

1
console.log('\uD83D\uDE00'); // => ''

高位代理码元的取值范围是从0xD8000xDBFF
低位代理码元的取值范围是从0xDC000xDFFF

代理对与代码点之间互相转换的算法如下所示:

Try in repl.it

1
2
3
4
5
6
7
8
9
10
11
12
13
function getSurrogatePair(astralCodePoint) {
let highSurrogate =
Math.floor((astralCodePoint - 0x10000) / 0x400) + 0xD800;
let lowSurrogate = (astralCodePoint - 0x10000) % 0x400 + 0xDC00;
return [highSurrogate, lowSurrogate];
}
getSurrogatePair(0x1F600); // => [0xDC00, 0xDFFF]
function getAstralCodePoint(highSurrogate, lowSurrogate) {
return (highSurrogate - 0xD800) * 0x400
+ lowSurrogate - 0xDC00 + 0x10000;
}
getAstralCodePoint(0xD83D, 0xDE00); // => 0x1F600

代理对并不是一个令人愉快的东西。在JavaScript中处理字符串时我们必须将它们视为特殊情况来处理,具体内容我们在下章细说。

但UTF-16的存储效率很高。因为99%需要处理的字符都属于BMP,只需要1个码元。

2.5 组合用字符

在一个书写系统的上下文中,一个字素或者符号是最小的可区分单元。

字素就是用户所认为的一个字符。屏幕上所展示的一个有形的字素称为图像字符(glyph)。

在大多数情况下,一个Unicode字符就代表一个字素。例如 U+0066 LATIN SMALL LETTER F就是一个英文字母f

但有时候一个字素会包含一系列字符。

例如å在丹麦语书写系统中是一个不可再分的字素。但它是用U+0061 LATIN SMALL LETTER A (渲染为a) 结合一个特殊字符U+030A COMBINING RING ABOVE(渲染为◌̊)来显示的。

U+030A用来修饰前一个字符,这种字符称为组合用字符

Try in repl.it

1
2
console.log('\u0061\u030A'); // => 'å'
console.log('\u0061'); // => 'a'

组合用字符是应用在前一个基础字符上以形成完整字素的字符。

组合用字符包括以下字符:重音符号、变音符、希伯来语点、阿拉伯语元音符号和印度语节拍符。

组合用字符通常不会离开基础字符单独使用。我们应该避免单独显示它们。

与代理对一样,在JavaScript中处理组合用字符也很棘手。

在用户看来一个组合字符序列(基础字符+组合用字符)是【一】个符号(例如'\u0061\u030A'就是'å')。但开发者必须清楚实际上要用到两个代码点U+0061U+030A来生成

Unicode basic terms
Unicode basic terms

Tutorial