by:T.J. Crowder
JavaScript 的字符串有时要比人们想的要稍复杂些。这个话题与我喜欢写的主题相关,所以写一篇关于 JavasSript 字符本质的文章将方便我在之后的文章中引用。
如果你已经阅读过《JavaScript: The New Toys》的第十章,这篇文章对你来说没有新的内容。如果你还没读过(为什么不读读看呢?😉),往下看吧……
摘要:
JavaScript 使用 Unicode 字符集,但是具体的处理细节决定了 JavaScript 中的“字符”并不一定是整个字符。这可能对像是将字符串拆分成单个字符或是将字符与字符串隔离(比如,将字符串的首字符大写)等操作产生非预期的影响。如果不小心处理 JavaScript 字符串,可能会破坏它们的内容。
正文:
JavaScript 使用 Unicode 表示文本。为了容纳世界上大量的字母、书写系统甚至是 emoji,Unicode 描述了非常广泛的字符,或者更一般的码位(code point)。最初,码位是 0 到 0xFFFF 的数字,即占用 16 个比特。但是这还不够,码位的范围被拓展为 0 到 0x10FFFF,即 21 个比特。(尽管现在还没有计划,这个数字可能还会增加。)为了避免系统强制使用三个或四个比特表示一个字符(为了对齐),Unicode 定义了转换格式(transformation formats),以将这 21 位编码成一个或多个较小的码元(code units)。UTF-8 使用一个字节的大小(8 bit)表示码元,这样可以表示许多西方字符,但其他的字符可能就需要两个、三个甚至是四个字节来表示。(例如,我们上面用到的“眨眼睛”的表情 😉 在 UTF-8 需要四个字节表示:0xF0 0x9F 0x98 0x89)。UTF-16 使用 16 比特表示一个码元,所以一个码位可能需要一个或两个码元表示。同样的“眨眼睛”的表情在 UTF-16 中表示为:0xD83D 0xDE09。(当然,还有比这更多的。)
回到 JavaScript,JavaScript 中的 String 是……:
……零个或有限多个 16 位无符号整数值的有序序列……序列中的每个值通常表示 UTF-16 中的单个 16 位的单元。
这意味着,有时可能需要用两个“字符”来表示一个码位:
const wink = "😉";
console.log(wink.length); // 2
人们经常说,JavaScript 的字符串都是 UTF-16 的,这几乎是正确的,这是因为在 UTF-16 中你不能单独拥有一个代理代码单元(surrogate code unit),而 JavaScript 除了规定值是 16 位整数之外,没有强制执行这个限制或任何其他的限制。这导致一些人说字符串是过时的 UCS-2 格式,而非 UTF-16。但是 JavaScript 确实会为某些操作将字符串解释为 UTF-16,而不是 UCS-2。类似地,使用 JavaScript 的环境通常将它们用作 UTF-16。所以我更愿意说它们是 UTF-16,但它们允许无效的代理代码单元。
无论哪种方式,都意味着你不能假设一个“字符”本身就是一个字符。例如,一个简单的“反转”字符串的代码通常看起来像这样:
const reverse = str => str.split("").reverse().join("");
但这会把包含代理对(和其他东西)的字符串搞得一团糟:
const wink = "😉";
const reversedWink = reverse(wink);
console.log(reversedWink); // Outputs two "unknown character" glyphs
还没完。除了码元的代理对可以组合成一个码位外,甚至还需要多个 Unicode 码位组合成特殊的“字符”(glyph)。例如,在印度和尼泊尔使用的梵文书写系统中,元音被写为修饰辅音字形的标记。码位 U+0928,न,发音为“na”,你可以在它后面加上码位 U+093F,产生 नि(“ni“)。(在本书的第十章还有更过细节)。
Happy coding!