专题 字符串 Unicode
你将知道:
- 什么是 Unicode 和 UTF-16
charCodeAt()
和codePointAt()
字符串方法length
属性的工作原理- Unicode 转义序列
- 词典比较
Unicode 和 UTF-16
我们已经知道字符串是字符序列。
计算机处理器内部发生的一切都是数字计算。字符串处理、图像处理、音频处理、视频处理——实际上一切都围绕着数字展开。
对于字符串中的字符,每个字符也都与一个特定的数字相关联。这种关联由所使用的字符集决定。
字符集可以被认为是一个包含字符名称及其相关数字的表。
如今,计算机中已存在众多已知甚至仍在使用的字符集,每种字符集都有其自身的传承。ASCII 无疑是迄今为止最流行的字符集之一,其历史可以追溯到计算机诞生之初。
在 ASCII 中,英文字母中的小写字母 'a' 与数字 97 相关联。大写字母 'A' 与数字 65 相关联。同样,小写字母 'b' 是 98,而大写字母 'B' 是 66。空格字符是 32。
Character name | Glyph | Code | Code (hex) | Character name | Glyph | Code | Code (hex) |
Space | 32 | 0x20 | Exclamation Mark | 33 |
| 0x21 | |
Quotation Mark |
| 34 | 0x22 | Number Sign | 35 |
| 0x23 |
Dollar Sign |
| 36 | 0x24 | Percent Sign | 37 |
| 0x25 |
Ampersand |
| 38 | 0x26 | Apostrophe | 39 |
| 0x27 |
Left Parenthesis |
| 40 | 0x28 | Right Parenthesis | 41 |
| 0x29 |
Asterisk |
| 42 | 0x2A | Plus Sign | 43 |
| 0x2B |
Comma |
| 44 | 0x2C | Hyphen-Minus | 45 |
| 0x2D |
Full Stop |
| 46 | 0x2E | Solidus | 47 |
| 0x2F |
Digit Zero |
| 48 | 0x30 | Digit One | 49 |
| 0x31 |
Digit Two |
| 50 | 0x32 | Digit Three | 51 |
| 0x33 |
Digit Four |
| 52 | 0x34 | Digit Five | 53 |
| 0x35 |
Digit Six |
| 54 | 0x36 | Digit Seven | 55 |
| 0x37 |
Digit Eight |
| 56 | 0x38 | Digit Nine | 57 |
| 0x39 |
Colon |
| 58 | 0x3A | Semicolon | 59 |
| 0x3B |
Less-Than Sign |
| 60 | 0x3C | Equals Sign | 61 |
| 0x3D |
Greater-Than Sign |
| 62 | 0x3E | Question Mark | 63 |
| 0x3F |
Commercial At |
| 64 | 0x40 | Latin Capital Letter A | 65 |
| 0x41 |
Latin Capital Letter B |
| 66 | 0x42 | Latin Capital Letter C | 67 |
| 0x43 |
Latin Capital Letter D |
| 68 | 0x44 | Latin Capital Letter E | 69 |
| 0x45 |
Latin Capital Letter F |
| 70 | 0x46 | Latin Capital Letter G | 71 |
| 0x47 |
Latin Capital Letter H |
| 72 | 0x48 | Latin Capital Letter I | 73 |
| 0x49 |
Latin Capital Letter J |
| 74 | 0x4A | Latin Capital Letter K | 75 |
| 0x4B |
Latin Capital Letter L |
| 76 | 0x4C | Latin Capital Letter M | 77 |
| 0x4D |
Latin Capital Letter N |
| 78 | 0x4E | Latin Capital Letter O | 79 |
| 0x4F |
Latin Capital Letter P |
| 80 | 0x50 | Latin Capital Letter Q | 81 |
| 0x51 |
Latin Capital Letter R |
| 82 | 0x52 | Latin Capital Letter S | 83 |
| 0x53 |
Latin Capital Letter T |
| 84 | 0x54 | Latin Capital Letter U | 85 |
| 0x55 |
Latin Capital Letter V |
| 86 | 0x56 | Latin Capital Letter W | 87 |
| 0x57 |
Latin Capital Letter X |
| 88 | 0x58 | Latin Capital Letter Y | 89 |
| 0x59 |
Latin Capital Letter Z |
| 90 | 0x5A | Left Square Bracket | 91 |
| 0x5B |
Reverse Solidus |
| 92 | 0x5C | Right Square Bracket | 93 |
| 0x5D |
Circumflex Accent |
| 94 | 0x5E | Low Line | 95 |
| 0x5F |
Grave Accent |
| 96 | 0x60 | Latin Small Letter A | 97 |
| 0x61 |
Latin Small Letter B |
| 98 | 0x62 | Latin Small Letter C | 99 |
| 0x63 |
Latin Small Letter D |
| 100 | 0x64 | Latin Small Letter E | 101 |
| 0x65 |
Latin Small Letter F |
| 102 | 0x66 | Latin Small Letter G | 103 |
| 0x67 |
Latin Small Letter H |
| 104 | 0x68 | Latin Small Letter I | 105 |
| 0x69 |
Latin Small Letter J |
| 106 | 0x6A | Latin Small Letter K | 107 |
| 0x6B |
Latin Small Letter L |
| 108 | 0x6C | Latin Small Letter M | 109 |
| 0x6D |
Latin Small Letter N |
| 110 | 0x6E | Latin Small Letter O | 111 |
| 0x6F |
Latin Small Letter P |
| 112 | 0x70 | Latin Small Letter Q | 113 |
| 0x71 |
Latin Small Letter R |
| 114 | 0x72 | Latin Small Letter S | 115 |
| 0x73 |
Latin Small Letter T |
| 116 | 0x74 | Latin Small Letter U | 117 |
| 0x75 |
Latin Small Letter V |
| 118 | 0x76 | Latin Small Letter W | 119 |
| 0x77 |
Latin Small Letter X |
| 120 | 0x78 | Latin Small Letter Y | 121 |
| 0x79 |
Latin Small Letter Z |
| 122 | 0x7A | Left Curly Bracket | 123 |
| 0x7B |
Vertical Line |
| 124 | 0x7C | Right Curly Bracket | 125 |
| 0x7D |
Tilde |
| 126 | 0x7E | Delete | 127 |
| 0x7F |
为了在字符和数字之间(即对字符进行编码 )以及在数字和字符之间(即对字符进行解码 )进行相互转换,我们应该使用某种编码-解码机制,也称为字符编码方案或字符编码格式 。
字符编码方案是在计算机上对字符进行编码和解码的系统。
一个更好、更流行的替代方案是 UTF-16 。在 UTF-16 编码方案中,每个字符至少占用 16 位。注意,16 位块代表一个代码单元 。毫无疑问,并非所有 Unicode 字符都能用 16 位来存储;我们必须增加存储空间来容纳它们。这就需要用到一些数学知识。UTF-16 是可变长度的,也就是说,有些字符占用一个码元(即 16 位),有些字符占用两个码元(即 32 位)。
在 Unicode 术语中,跨越两个代码单元的字符被表示为代理对 - 一个高代理 值(第一个代码单元)后跟一个低代理 值(第二个代码单元)。
问题在于,UTF-16 中的某些代码单元(即 16 位块)被保留用于特殊含义——有些是高位代理,有些是低位代理。当 UTF-16 解码器遇到高位代理时,它会立即知道也应该读取下一个代码单元,以确定当前字符。
回到 JavaScript,它默认对其字符串类型使用 UTF-16 编码方案 。
因此,Unicode 是使用的字符集。这样,JavaScript 中给定字符串中的每个字符占用 16 位或 32 位,具体取决于字符本身。
所以从 JavaScript 的角度来看,字符串是无符号的 16 位整数序列 。
换句话说,JavaScript 字符串是 Unicode 代码单元(无符号 16 位整数)的序列。在使用字符串时,务必牢记这一定义。
UTF-16 解码器(将字符转换为数字)将给定的字符串读取为 16 位整数数组,并处理每个代码单元以确定应该输出的相应字符。
所以最后,我们现在了解了 JavaScript 中 “字符” 的含义,即它是一个基于 UTF-16 格式的跨越 16 位或 32 位的无符号整数。
现在,让我们研究一下字符串上可用的 charCodeAt()
方法,它可以帮助我们看到这些无符号整数。 不然太抽象
charCodeAt()
方法
借助 charCodeAt()
字符串方法,我们可以检查与字符串中特定字符相关联的代码单元(即数字)。
string.charCodeAt([index])
只需将您想要检查其代码的字符的索引作为第一个 index
参数传入,该方法将根据 UTF-16 格式返回与该字符相对应的整数。
var str = 'abc';console.log(str.charCodeAt(0)); 97
console.log(str.charCodeAt(1)); 98
console.log(str.charCodeAt(2)); 99
如果我们向 charCodeAt()
传递一个在给定字符串中不存在的索引,则该方法返回 NaN
。
显然, 'abc'
中没有第四个字符,同样 str.charCodeAt(3)
计算结果为 NaN
。
现在,我们尝试一个稍微复杂一点的例子。考虑以下代码:
var str = '🙂';console.log(str.charCodeAt(0));
console.log(str.charCodeAt(1));
let we see see
55357
56898
这真是令人惊讶。上面的字符串中只有一个字符,因此 str.charCodeAt(1)
应该返回 NaN
。然而,事实并非如此。
what's going on !
嗯,这只是一个问题,或者正确定义 charCodeAt()
作用。在开头,我们说过 charCodeAt()
返回给定字符串中特定字符的代码。这个定义并不精确。更好的说法是 charCodeAt( i )
返回给定字符串中的 i
个代码单元。
JavaScript 将字符串视为代码单元(16 位块)的序列。现在,无论字符串包含两个代码单元来表示一个字符,还是包含两个代码单元来表示两个独立的字符,它都是由两个单元组成的,因此我们可以分别访问它们。
回到上面的代码,🙂字符由两个代码单元组成,它们共同构成一个代理对。第一个代码单元是高位代理,值为 55,357,而第二个代码单元是低位代理,值为 56,898。这两个代码单元共同构成了 UTF-16 编码的 🙂 字符。
例如,如果我们使用括号表示法访问字符串 '🙂'
的第一个或第二个字符,我们会得到相同的行为:
var str = '🙂'
str[0] >>> '\uD83D'
str[1] >>> '\uDE42'
// 十六进制数 D83D 代表十进制数 55,357,而 DE42 代表 56,898,
// 分别类似于上面的 charCodeAt(0) 和 charCodeAt(1) 调用发出的值。
如果我们知道给定的字符串中有一个字符的代码点超出了 16 位范围(即 65,535),使用 2 个代码单元表示,那么我们必须使用 codePointAt()
方法来检索整个代码点。
codePointAt()
方法
codePointAt()
方法与 charCodeAt()
非常相似,因为它也允许我们检查字符串中给定字符的代码。
string.codePointAt([index])
index
是给定字符串中要检查的代码单元的索引。如果省略,则默认为 0
。
然而, codePointAt()
和 charCodeAt()
之间存在细微但非常重要的差异。
也就是说,如果检查的代码单元代表高代理值, codePointAt()
会将以下低代理代码单元与高代理代码单元(检查的代码单元)结合起来,生成完整的代码点。
var str = '🙂';console.log(str.codePointAt(0)); // 128578
console.log(str.codePointAt(1)); // 56898
注意这里的第一条日志。由于字符 '🙂' 由两个代码单元组成,并且在 str.codePointAt(0)
中我们检查了第一个代码单元,它是一个高位代理,该方法将下一个代码单元(其值为 56,898)与高位代理组合起来,得到 128,578('🙂' 在 Unicode 中的代码点)。
如果检查的代码单元不代表高位代理, codePointAt()
会按原样返回该单元。同样,对于字符串 '🙂'
, str.codePointAt(1)
和 str.charCodeAt(1)
都会返回相同的值。
除此之外,如果检查的代码单元不存在于给定的字符串中,则 charCodeAt()
返回 NaN
, codePointAt()
返回 undefined
。
length
属性
length
字符串属性返回给定字符串中的代码单元总数。
因此,字符串 '🙂'
的长度不是 1
;而是 2
,因为 '🙂'
由两个代码单元组成。
console.log('abc'.length) // 3
console.log('🙂'.length) // 2
请注意,上面的 “字符” 数量并不总是与 length
的返回值(即代码单元的数量)相同。
如果我们想要获得给定字符串中的字符总数(而不是代码单元),我们必须简单地遍历整个字符串,检查每个单独的代码单元以确定它是否代表高代理值(因此应该与下一个代码单元组合以生成单个字符)。
Unicode 转义序列
当我们拥有特定字符的十六进制代码并想要展示它时,Unicode 转义序列很有用。
\u{X}
\u{XX}
...
\u{XXXXXX}
词典比较
我们了解了关系运算符 <
、 <=
、 >
、 >=
、 ===
和 !==
在字符串上的使用。现在,是时候深入了解它们的工作原理了。上面列出的每个二元运算符都对给定的字符串执行所谓的字典顺序比较。这或多或少类似于字典按字母顺序对单词进行排序的方式。
词典顺序比较是如何发生的?
对给定的两个字符串同时进行迭代,并将相应的代码单元相互比较。
如果在任何时候发现不匹配,则代码单元值大于另一个字符串中相应代码单元的字符串在字典顺序上被称为大于另一个字符串;相应地,另一个字符串在字典顺序上被称为较小 。
但是,如果直到迭代耗尽其中一个字符串(即到达其末尾)时才发现不匹配,那么此时就有两种可能性:
- 两个字符串的长度相同。在这种情况下,字符串完全相同。
- 两个字符串的长度不相同。在这种情况下,长度较大的字符串按字典顺序排列比另一个字符串大;相应地,另一个字符串按字典顺序排列则较小。
回到运算符,给定两个字符串变量 a
和 b
:
a < b
表示 'a
按字典顺序小于b
' 。 事实上 比如 'a' 97 'b' 98 97 < 98a > b
表示 'a
按字典顺序大于b
' 。a === b
表示 'a
与b
相同 ' 。
其他关系运算符 <=
、 >=
和 !==
可以从这三个简单运算符派生而来。
'a' < 'b' // true
'a' < 'a' // false
'a' < 'A' // false
'ab' < 'abc' // true
在第三个语句中,由于 'a'
的代码单元(十进制为 97)大于 'A'
的代码单元(十进制为 65), 'a' < 'A'
得出 false
。
在最后一条语句中,由于 'ab'
和 'abc'
的前两个代码单元没有不匹配的情况,因此比较的重点在于字符串的长度。由于 'ab'
长度较小,因此按字典顺序排列它确实比 'abc'
短。因此, 'ab' < 'abc'
返回 true
。