本节深入探讨字符串内部。如果你计划处理表情符号、罕见的数学或象形文字或其他罕见符号,那么这些知识对你来说会很有用。
正如我们所知,JavaScript 字符串基于 Unicode:每个字符由 1-4 个字节的字节序列表示。
JavaScript 允许我们通过使用以下三种表示法之一指定其十六进制 Unicode 代码来将字符插入字符串
-
\xXX
XX
必须是两个十六进制数字,其值介于00
和FF
之间,然后\xXX
是 Unicode 代码为XX
的字符。由于
\xXX
表示法仅支持两个十六进制数字,因此它只能用于前 256 个 Unicode 字符。这前 256 个字符包括拉丁字母、大多数基本语法字符以及其他一些字符。例如,
"\x7A"
与"z"
(UnicodeU+007A
)相同。alert( "\x7A" ); // z alert( "\xA9" ); // ©, the copyright symbol
-
\uXXXX
XXXX
必须恰好是 4 个十六进制数字,其值介于0000
和FFFF
之间,则\uXXXX
是 Unicode 编码为XXXX
的字符。Unicode 值大于
U+FFFF
的字符也可以用此表示法表示,但在此情况下,我们需要使用所谓的代理对(我们将在本章后面讨论代理对)。alert( "\u00A9" ); // ©, the same as \xA9, using the 4-digit hex notation alert( "\u044F" ); // я, the Cyrillic alphabet letter alert( "\u2191" ); // ↑, the arrow up symbol
-
\u{X…XXXXXX}
X…XXXXXX
必须是介于0
和10FFFF
(Unicode 定义的最高代码点)之间的 1 到 6 个字节的十六进制值。此表示法使我们能够轻松表示所有现有的 Unicode 字符。alert( "\u{20331}" ); // 佫, a rare Chinese character (long Unicode) alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long Unicode)
代理对
所有常用字符都有 2 字节代码(4 个十六进制数字)。大多数欧洲语言中的字母、数字以及基本的统一 CJK 表意文字集(CJK - 来自中文、日文和韩文书写系统)具有 2 字节表示法。
最初,JavaScript 基于 UTF-16 编码,每个字符仅允许 2 个字节。但 2 个字节仅允许 65536 种组合,这不足以表示 Unicode 的每个可能符号。
因此,需要超过 2 个字节的稀有符号使用一对称为“代理对”的 2 字节字符进行编码。
作为副作用,此类符号的长度为 2
alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X
alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY
alert( '𩷶'.length ); // 2, a rare Chinese character
这是因为在创建 JavaScript 时不存在代理对,因此语言无法正确处理它们!
我们实际上在上面每个字符串中都有一个符号,但 length
属性显示的长度为 2
。
获取符号也可能很棘手,因为大多数语言特性将代理对视为两个字符。
例如,这里我们可以在输出中看到两个奇异字符
alert( '𝒳'[0] ); // shows strange symbols...
alert( '𝒳'[1] ); // ...pieces of the surrogate pair
代理对的各个部分彼此之间没有意义。因此,上面示例中的警报实际上显示的是垃圾。
从技术上讲,代理对也可以通过其代码检测:如果一个字符的代码在 0xd800..0xdbff
区间内,那么它就是代理对的第一部分。下一个字符(第二部分)的代码必须在 0xdc00..0xdfff
区间内。这些区间由标准专门为代理对保留。
因此,JavaScript 中添加了 String.fromCodePoint 和 str.codePointAt 方法来处理代理对。
它们本质上与 String.fromCharCode 和 str.charCodeAt 相同,但它们正确地处理了代理对。
这里可以看到区别
// charCodeAt is not surrogate-pair aware, so it gives codes for the 1st part of 𝒳:
alert( '𝒳'.charCodeAt(0).toString(16) ); // d835
// codePointAt is surrogate-pair aware
alert( '𝒳'.codePointAt(0).toString(16) ); // 1d4b3, reads both parts of the surrogate pair
也就是说,如果我们从位置 1 开始(这里相当不正确),那么它们都只返回对的第 2 部分
alert( '𝒳'.charCodeAt(1).toString(16) ); // dcb3
alert( '𝒳'.codePointAt(1).toString(16) ); // dcb3
// meaningless 2nd half of the pair
您将在本章 可迭代对象 的后面找到更多处理代理对的方法。可能也有专门的库可以处理,但没有足够出名可以在这里建议。
我们不能只在任意位置拆分字符串,例如,获取 str.slice(0, 4)
并期望它是一个有效的字符串,例如
alert( 'hi 😂'.slice(0, 4) ); // hi [?]
这里我们可以在输出中看到一个垃圾字符(笑脸代理对的前半部分)。
如果您打算可靠地使用代理对,请注意这一点。可能不是什么大问题,但至少您应该了解会发生什么。
变音符号和规范化
在许多语言中,有一些符号是由带有上面/下面的标记的基本字符组成的。
例如,字母 a
可以是这些字符的基本字符:àáâäãåā
。
大多数常见的“复合”字符在 Unicode 表中都有自己的代码。但并非所有字符都有,因为可能的组合太多了。
为了支持任意组合,Unicode 标准允许我们使用多个 Unicode 字符:基本字符后跟一个或多个“标记”字符来“装饰”它。
例如,如果我们有 S
后跟特殊“上点”字符(代码 \u0307
),它显示为 Ṡ。
alert( 'S\u0307' ); // Ṡ
如果我们需要在字母上方(或下方)添加额外的标记——没问题,只需添加必要的标记字符即可。
例如,如果我们追加一个“点在下方”(代码 \u0323
)的字符,那么我们就会得到“带上下点的 S”:Ṩ
。
例如
alert( 'S\u0307\u0323' ); // Ṩ
这提供了很大的灵活性,但也带来了一个有趣的问题:两个字符在视觉上可能看起来相同,但用不同的 Unicode 组合表示。
例如
let s1 = 'S\u0307\u0323'; // Ṩ, S + dot above + dot below
let s2 = 'S\u0323\u0307'; // Ṩ, S + dot below + dot above
alert( `s1: ${s1}, s2: ${s2}` );
alert( s1 == s2 ); // false though the characters look identical (?!)
为了解决这个问题,存在一个“Unicode 规范化”算法,它将每个字符串转换为单个“正常”形式。
它由 str.normalize() 实现。
alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true
有趣的是,在我们的情况下,normalize()
实际上将一个 3 个字符的序列合并为一个:\u1e68
(带两个点的 S)。
alert( "S\u0307\u0323".normalize().length ); // 1
alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true
实际上,情况并非总是如此。原因是符号 Ṩ
“相当常见”,因此 Unicode 创建者将其包含在主表中并为其提供了代码。
如果你想了解有关规范化规则和变体的更多信息,它们在 Unicode 标准的附录中进行了描述:Unicode 规范化形式,但对于大多数实际目的,本节中的信息就足够了。
评论
<code>
标记,对于多行代码,请将其包装在<pre>
标记中,对于 10 行以上的代码,请使用沙盒(plnkr、jsbin、codepen…)