skip to content
usubeni fantasy logo Usubeni Fantasy

Unicode 编码及其在 Web 前端的应用

/ 16 min read

阅读本文前,请先熟悉各进制间的转换,否则看起来会有点懵 😂

Unicode

相关:UCS(Universal Character Set)原本标准不同,但现在已经与 Unicode 统一

Unicode 就是一种世界统一的字符编码集合,在这个集合里,世界上每一个字符——任何语言的文字、符号甚至 emoji,都有自己的编号,称为 code point(码点)。这样的编号从 0000 开始,到 10FFFF,可以容纳大于一百万个不重复的字符。

点击表中链接可以查看某段位置包含的字符。其中,从 0000ffff 是最常用是平面 0,也称作 Basic Multilingual Plane(BMP),大多数常用汉字都包含在其中。

我们用 Unicode 表示一个字符,约定俗成地 U+ 加上这个字的 16 进制码点,例如“汉”就是 U+6C49

但是 Unicode 虽然给字符编码了,但是在数据传输时,仅仅是给数据一个号码是不够的,还要想出一种让计算机看懂这个号码的方法,这就引出了 UTF(Unicode Transformation Format),也就是 Unicode 的传输格式。

在 Unicode 维基页的 Mapping and encodings 一节,可以看到多种编码方式,现在常见的 UTF 有 UTF-8、UTF-16、UTF-32,三种格式各有特色。

其中最容易理解的反倒是最不常用的 UTF-32,所以下面会先用 UTF-32 举例——但在此之前,现说一下 code unit(码元)

The minimal bit combination that can represent a unit of encoded text for processing or interchange. The Unicode Standard uses 8-bit code units in the UTF-8 encoding form, 16-bit code units in the UTF-16 encoding form, and 32-bit code units in the UTF-32 encoding form.

上面是 Unicode Consortium 给出的定义,粗略翻译过来就是编码一个字符的最小单元。对于 UTF-8 码元是 8-bit,UTF-16 码元是 16-bit,UTF-32 码元是 32-bit。

UTF-32

UTF-32(UCS-4)是一种空间效率极低、长度不变的编码方式,码元长度为 32-bit,意思就是编码一个字符至少要 32 位,举个例子:

a 的 UTF-32 编码是 00000061(16 进制)0000 0000 0000 0000 0000 0000 0110 0001(2 进制,为了方便阅读,添加了空格)

同理可得 ab 就是 0000 0000 0000 0000 0000 0000 0110 0001 0000 0000 0000 0000 0000 0000 0110 0010

那么就很明显可以看出来了,前面的一大堆 0 就是浪费空间的元凶。

看到这里可能会有人提出,把前面的 0 都删掉不就好了吗?

还是上面的例子,ab 删掉 0 之后是 0110 0001 0110 0010

问题就来了,谁知道你相邻的 byte 是单个字符还是分别是几个字符呢?0110 0001 0110 0010 不只可以是 ab 也可以是编号为 24930 的那个字符。

所以我们还必须加上一些标志,不能简单地删除前面的 0 了事,当然,加标志就是其他的编码方法了,我们来看看广泛使用的 UTF-8。

UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种特别广泛使用的格式,码元长度为 8-bit,是一种可变长度的编码方式,它的特点是兼容 ASCII。

对于 BMP 的字符,UTF-8 会将其编码为 1 到 3 个码元,非 BMP 编码为 4 个码元。

这就是上面说到的“加标志”,让计算机看懂前后码元到底是一个字符还是多个码元组成一个字符:

  • 0 开头,单独一个码元(这部分兼容 ASCII)
  • 110 开头,与后面 1 个码元一起组成一个字符
  • 1110 开头,与后面 2 个码元一起组成一个字符
  • 11110 开头,与后面 3 个码元一起组成一个字符
Number
of bytes
First
code point
Last
code point
Byte 1 Byte 2 Byte 3 Byte 4
1 U+0000 U+007F 0xxxxxxx
2 U+0080 U+07FF 110xxxxx 10xxxxxx
3 U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面是具体的编码例子,基本套路就是:

  1. 写成二进制
  2. 在特定的位置拆分开(表中紫、蓝、绿、红数字)
  3. 添加标志拼接起来(表中黑色数字)
Character Code point UTF-8
Octal Binary Binary Octal Hexadecimal
$ U+0024 044 010 0100 00100100 044 24
¢ U+00A2 0242 000 1010 0010 11000010 10100010 302 242 C2 A2
U+0939 004471 0000 1001 0011 1001 11100000 10100100 10111001 340 244 271 E0 A4 B9
U+20AC 020254 0010 0000 1010 1100 11100010 10000010 10101100 342 202 254 E2 82 AC
U+D55C 152534 1101 0101 0101 1100 11101101 10010101 10011100 355 225 234 ED 95 9C
𐍈 U+10348 0201510 0 0001 0000 0011 0100 1000 11110000 10010000 10001101 10001000 360 220 215 210 F0 90 8D 88

UTF-16

UTF-16(UCS-2)即便没有 UTF-8 使用得广泛,仍是一个比较常用的编码方式。

以 UTF-16 16-bit 的码元长度,BMP 字符可以统一用一个码元表示,但是这样 a 就会被编码为 0000 0000 0110 0001,因此不兼容 ASCII。

但是对于 BMP 后排大户——汉字,UTF-8 会将 U+0800 到 U+FFFF 字符编码为 3 byte,UTF-16 则是稳定的 2 byte,所以如果文件内包含大量中文文本,编码为 UTF-16 会比 UTF-8 的体积会显著缩小。

下表是 UTF-16 的编码方式,可以说与 UTF-8 大同小异。BMP 以外的字符分割后分别添加 110110110111 提示计算机两个码元组成一个字符。

Character Binary code point Binary UTF-16 UTF-16 hex
code units
UTF-16BE
hex bytes
UTF-16LE
hex bytes
$ U+0024 0000 0000 0010 0100 0000 0000 0010 0100 0024 00 24 24 00
U+20AC 0010 0000 1010 1100 0010 0000 1010 1100 20AC 20 AC AC 20
𐐷 U+10437 0001 0000 0100 0011 0111 1101 1000 0000 0001 1101 1100 0011 0111 D801 DC37 D8 01 DC 37 01 D8 37 DC
𤭢 U+24B62 0010 0100 1011 0110 0010 1101 1000 0101 0010 1101 1111 0110 0010 D852 DF62 D8 52 DF 62 52 D8 62 DF

html

格式描述
€&#x + 十六进制 + ;
€&# + 十进制 + ;
€& + 名称 + ;

转义标志是 &,不管你的 html 文件使用何种编码方式(例如这里写成 <meta http-equiv="Content-Type" content="text/html; charset=shift_jis">),转义使用的都是Unicode 码点

使用名称的话可以看这个可以转义的名称列表

这样的 html 文档内的转义常用于代替空格、<>&" 等 html 里有功能的字符,但是当然不止如此。

iconfont 是前端开发者很熟悉的平台,这个平台可以把图标做成字体,引入这个字体,使得每个图标有一个特定的 Unicode 码位,只要使用转义字符,就能顺利显示该图标。

利用同样的原理,你也可以在 React Native 使用阿里 iconfont 图标

CSS

CSS 中的转义标志是 \

格式描述
\20AC如果下一位是十六进制可用字符必须加空格
\0020AC必须六位,后面可以不加空格

例如 css 选择器本不可以以数字开头,但是使用转义字符就能选择 class 123

.\31 23 { ... }.\00003123 { ... }

字符串

大家熟悉字符串转义应该有这些:处理字符串引号冲突的 \' \" 换行和拉开距离的 \n \t

下面是使用 Unicode 码点转义成字符的四种方法(使用场景一般也是上面提到的插入 iconfont):

格式描述
\XXX仅限 ISO-8859-1 范围(Unicode U+0000 到 U+00FF),1~3 位奇葩的八进制
\uXXXXUnicode U+0000 到 U+FFFF(BMP),4 位十六进制
\u{X} ... \u{XXXXXX}Unicode U+0000 到 U+10FFFF,1 到 6 位十六进制
\xXX仅限 ISO-8859-1 范围(Unicode U+0000 到 U+00FF),2 位十六进制
console.log("\121");
// => Q
console.log("\u9e6b");
// => 鹫
console.log("\u{1f602}");
// => 😂
console.log("\xea");
// => ê

说到 JavaScript 便顺带一提 codePointAt() 和 charCodeAt()的区别:

console.log("".charCodeAt().toString(2));
console.log("".codePointAt().toString(2));
// => 101010000001001

BMP 内,charCodeAtcodePointAt 返回的结果相等。

console.log("𠮷".charCodeAt(0).toString(2));
// => 1101100001000010
console.log("𠮷".charCodeAt(1).toString(2));
// => 1101111110110111
console.log("𠮷".codePointAt(0).toString(2));
// => 100000101110110111
console.log("𠮷".codePointAt(1).toString(2));
// => 1101111110110111

BMP 外,字符被分成两块,charCodeAt 的两个结果中明显看到 110110110111,便能推测出这是 UTF-16。

codePointAt 如其名,拿到的直接就是 Unicode 码点,也不用分两位来取了,codePointAt(1) 是没有意义的。

还原方法如下:

console.log("\ud842\udfb7"); //𠮷,
console.log("\u{20bb7}"); //𠮷

参考与拓展

评论组件加载中……