Unicode——字符串内幕
先看图,这是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:自然数序列、电脉冲), 以便文本在计算机存储和通过通信网络的传递。
-
字符集和字符编码的的关系:
-
字符集是书写系统字母符号的集合。
-
字符编码是将字符映射为一特定的字节或字节序列,是一种规则。
-
比如: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十六进制码点值;,对于部分字符,还可以用&实体名称;表示。如下
|显示结果|描述|实体名称|实体编码|
:---|:---|:---|:---|
| 空格 | |   |
< | 小于号 | < | \< |
> | 大于号 | > | \> |
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.fromCodePoint 和 String.codePointAt 这两个方法来处理代理对。
它们本质上与 String.fromCharCode 和 String.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 主表中,并为其提供了对应的编码。
六、总结
- 在允许的情况下,始终优先选择输入Unicode字符本身,而不是转义序列或字符实体。
- 如果处理的字符串包含Emoji或多种语言的,获取字符串的长度可能会出现意外的错误。
- 参考文档: