本节深入探讨字符串内部。如果你计划处理表情符号、罕见的数学或象形文字或其他罕见符号,那么这些知识对你来说会很有用。
正如我们所知,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 规范化形式,但对于大多数实际目的,本节中的信息就足够了。