Unicode——字符串内幕

484 阅读9分钟

Unicode——字符串内幕

先看图,这是2021年最常用的表情符号。The Most Frequently Used Emoji of 2021

The Most Frequently Used Emoji of 2021

  • 要是需要你用字符来表示,你知道这些表情背后的编码都是什么吗?

如果打算处理表情符号(emoji)、罕见的数学或象形文字字符,或其他罕见字符。 下面这些知识对你很有用!

一、背景

计算机起源于美国,他们对英语字符与二进制位之间做了统一规定, 并制定了一套字符编码规则,这套编码规则就是ASCII编码。

ASCII编码一共定义了128个字符的编码规则,用**(0x00 - 0x7F)**表示, 这些字符组成的集合就叫做ASCII字符集。

随着计算机普及,不同地区合国家出现很多字符编码,比如: 中国大陆的GB2312、港台的BIG5等。

问题来了:不同的字符编码,同一个二进制数据,不同的编码会解析出不同的字符。 这使得计算机在不同国家交流变得困难。

解决:对所有的国家和地区的字符进行编码,于是Unicode就出现了。

二、定义

Unicode:是国际标准字符集,他将世界各种语言的每个字符定义一个唯一的编码, 以满足跨语言、跨平台的文本信息转换。

Unicode字符集的编码范围是 0x0000 - 0x10FFFF,可以容纳一百多万个字符。 每个字符都有一个二进制数值和它对应,这里的二进制数值也叫 码点。 比如在ASCII字符集中,字母A经过ASCII编码得到的值是65,那么65就是字符A在ASCII字符集中的码点。

2.1 怎么表示表情(emoji)、罕见的数学、或者象形文字字符

javaScript允许我们通过以下三种方式,将一个字符以十六进制Unicode编码插入到字符串中。

1.0xXX

XX 必须是介于 00 - FF 之间的两位十六进制数。0xXX 表示Unicode编码为 XX 的字符。

思考一下: 00 - FF 只支持十六进制数。那它能表示多少个字符呢?

2.\uXXXX XXXX必须是4位十六进制数,\uXXXX 表示Unicode编码为 XXXX 的字符。

  • U+表示紧跟在后面的十六进制数是Unicode的码点。

  • 这么多符号,Unicode不是一次性定义的,而是区分定义。每个区可以存放65536 个(2^16)字符, 称为一个平面。目前,一共有17个平面。也就是说,现在Unicode字符集大小现在是2^21。

  • 最前面的 65536 个字符位,称为基本平面(缩写 BMP),它的码点范围是从 0 一直到 2^16-1,写成 16 进制就是从 U+0000 到 U+FFFF。所有最常见的字符都放在这个平面,这是 Unicode 最先定义和公布的一个平面。

  • Unicode 值大于 U+FFFF 的字符也可以用这种方法来表示,但在这种情况下,我们要用到代理对

3.\u{X…XXXXXX} X…XXXXXX 必须是介于 0 和 10FFFF(Unicode定义的最高码位)之间的1-6个字节的十六进制值。这种方式可以表示需所有现有的Unicode字符。

想要知道哪个字符对应的Unicode编码可以查询Unicode字符编码查询

  • ✌ დ✍ 这些字符又该用字符来表示呢?

~~🌰 \u{270c} \u{10d3}\u{270d} '✌ დ✍'

三、字符集和字符编码

  • 字符集:多个字符的集合,比如GB2312是简体中文的字符集。

  • 字符编码:把字符集中的字符编码为(映射)指定集合中的某一对象(eg:自然数序列、电脉冲), 以便文本在计算机存储和通过通信网络的传递。

  • 字符集和字符编码的的关系

    1. 字符集是书写系统字母符号的集合。

    2. 字符编码是将字符映射为一特定的字节或字节序列,是一种规则。

比如:Unicode只是字符集,UTF-8、UTF-16、UTF-32才是真正的字符编码规则。

3.1 UTF-8编码(最小单位1字节)

UTF-8是万维网上最常用的字符编码。每个字符由1-4个字节表示(1字节=8bit)。UTF-8与ASCII向后兼容,可以表示任何标准的Unicode字符。

3.1.1 编码规则:

UTF-8是Unicode的一中实现方式,所以,想要对一个字符进行UTF-8编码。 首先我们要知道这个字符的Unicode编码(字符的Unicode编码是约定好的,全球统一不变的)。 想要知道某个字符的Unicode的编码方式也很简单,工具

针对不同的 unicode 符号范围,utf-8 编码实际占用的字节数可能 1~4 字节不等👇

unicode 符号范围 | utf-8 编码方式
00000000 ~ 0000007F | 0xxxxxxx 
00000080 ~ 000007FF | 110xxxxx 10xxxxxx 
00000800 ~ 0000FFFF | 1110xxxx 10xxxxxx 10xxxxxx 
00010000 ~ 0010FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 

  • 对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。因此,对于英文中的 0 - 127 号字符,与 ASCII 码完全相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开完全没有问题。

  • 对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为 0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。

十六进制转二进制

看例子:
A 查看Unicode是     -->   \u0041 

所以对应UTF-8编码是 --> 0100 0001


梁 Unicode --> \u6881 

---> 11100110 10100010 10000001

汉 Unicode --> \u6c49

---> 11100110 10110001 10001001 0xE6B189 
(\u6c49通过UTF-8编码器进行编码,最后输出的UTF-8编码就是E6B189)

以【梁】举例,我们查到【梁】的Unicode编码是 \u6881 (\u只是Unicode的编码标记,其后面跟的还是16进制数) 发现【梁】的编码范围是 0x0800 ~ 0xffff 间,故选择 3字节 的编码方式

将 \u6881 转化为2进制(共16位),并分成 3、6、6 三组,填入👆编码方式的「x」 的位置, 得到 11100110 10100010 10000001 ,至此完成【梁】 的 utf-8 编码,转化为 16 进制描述即:0xe6a281。

二进制转十六进制

因为2的4次方等于16,所以把二进制数转化为十六进制时, 每四位合为一位,转化成十进制数,然后记作对应的十六进制数。

对应关系:0—9,10—A,11—B,12—C,13—D,14—E,15—F

四、Unicode的应用

4.1 Unicode in HTML

在HTML中,除了直接输入字符本身,所有的Unicode字符还可以用字符实体表示。 &#十进制码点值;或&#x十六进制码点值;,对于部分字符,还可以用&实体名称;表示。如下

|显示结果|描述|实体名称|实体编码|
:---|:---|:---|:---|
  | 空格   |   |   |
< | 小于号 | &lt;   | \&#60; |
> | 大于号 | &gt;   | \&#62; |

4.2 Unicode in CSS

在CSS的字符串中,也可以用转义序列来表示一个Unicode字符,格式为\十六进制码点值 比如 ↻

.foo::after { content: ' \21BA' /* ↻ */ }


五、代理对

所有常用字符都有对应的2字节长度的编码(4位十六进制数)。

大多数欧洲语言的字母、数字都有对应的2字节长度的Unicode编码。

最初,JavaScript是基于UTF-16编码的,只允许每个字符占2个字节长度。 但2个字节只允许65536种组合,这对于便是Unicode里每个可能的符号来说,是不够的。

所以,需要使用超过2个字节长度来表示稀有符号。这就是使用一对2字节长度的字符编码, 它被称为“代理对”。

这样子也有副作用——这些符号的长度是2

console.log('🌰'.length) // 2
console.log('😂'.length) // 2 
console.log('𩷶'.length) // 2

如何获取这些符号,也是一个棘手的问题。

'🌰'[0] // 奇怪的符号……
'🌰'[1]

代理对的片段失去彼此就没有意义。

  • 代理对编码检测代理对

    • 从技术上讲
      • 代理对前一部分:字符的编码在 0xd800-0xdbff 范围

      • 代理对后一部分:字符的编码在 0xdc00-0xdfff 范围

JavaScript 新增了 String.fromCodePointString.codePointAt 这两个方法来处理代理对。

它们本质上与 String.fromCharCodeString.charCodeAt 相同,但它们可以正确地处理代理对。

alert( '𝒳'.charCodeAt(0).toString(16) ); // d835 // codePointAt 可以正确处理代理对
alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3,读取到了完整的代理对

5.1 变音符号和规范化

举个栗子🌰,字母a,就是这些字符 àáâäãåā 的基础字符

为了支持任意的组合,Unicode标准允许我们使用多个Unicode字符: 基础字符后跟着一个或多个“装饰”它的“标记”字符。

举个栗子🌰,如果在 S 后附加上特殊的“上方的点”字符,(编码为\u0307),则显示为 Ṡ

console.log( 'S\u0307' ) // Ṡ

console.log( 'S\u0307\u0323' ) // Ṩ

问题: 这个组合的方式就很灵活,但是也有个有趣的问题。

let s1 = 'S\u0307\u0323'; // Ṩ, S + 上方点符号 + 下方点符号
let s2 = 'S\u0323\u0307'; // Ṩ, S + 下方点符号 + 上方点符号

alert( `s1: ${s1}, s2: ${s2}` );

alert( s1 == s2 ); // 尽管这两个字符在我们看来是相通的,但结果却是 false

解决: Unicode规范化 算法 ,就是解决这个问题的。

let s1 = 'S\u0307\u0323';
let s2 = 'S\u0323\u0307';
s1 = s1.normalize('NFD');
s2 = s2.normalize('NFD');
alert( s1 == s2 );

可以看下这个方法来实现。str.normalize()

但是不是总能得到true 出现这种情况的原因是符号 Ṩ 是“足够常见的”,所以 Unicode 创建者将其囊括在了 Unicode 主表中,并为其提供了对应的编码。

六、总结

  1. 在允许的情况下,始终优先选择输入Unicode字符本身,而不是转义序列或字符实体
  2. 如果处理的字符串包含Emoji或多种语言的,获取字符串的长度可能会出现意外的错误。
  • 参考文档:

Unicode

Unicode 规范化形式_

unicode编码(unicode完整编码表)

Unicode、UTF-8、UTF-16 终于懂了

艺术鬼才!Unicode 字符还能这么玩?