为什么在JavaScript中 "👩🏾🌾" 的长度为7?
在JavaScript中,字符串长度属性的行为可能会让人感到困惑,尤其是涉及包含表情符号的字符串时。本文将解释为什么某些情况下字符串的长度可能会出现意外的结果。
什么是扩展图形簇?
首先,让我们了解一些Unicode术语。扩展图形簇通常可以被称为字符。在JavaScript中,我们通常认为一个字符的长度为1,但事实并非总是如此。
为什么长度会令人意外?
在JavaScript中,我们使用字符串的 length 属性来确定其长度。有时,结果是直观的,例如:
"E".length; // => 1
"♬".length; // => 1
然而,有些情况下,结果可能会令人感到意外,例如:
"🌸".length; // => 2
"👩🏾‍🌾".length; // => 7
要理解其中的原因,您需要了解一些Unicode的术语和背景知识。
扩展图形簇和标量值
扩展图形簇由一个或多个标量值(scalars)组成。标量值是介于0和1114111之间的整数,尽管目前许多值尚未使用。许多扩展图形簇仅包含一个标量值,例如字符 E,其标量值为69。然而,一些扩展图形簇由多个标量值组成,例如字符 👩🏾🌾,由四个标量值组成:128105、127998、8205和127806。
UTF-16编码单元
JavaScript内部使用UTF-16编码单元来存储这些标量值。每个UTF-16编码单元都是一个16位无符号整数,取值范围为0到65535。许多标量值可以适应一个编码单元,但一些较大的标量值必须被拆分成两个编码单元。这些被拆分的情况被称为“代理对(surrogate pairs)”。
"👩🏾🌾" 的例子
以字符串 "👩🏾🌾" 为例。该字符串由四个标量值组成:128105、127998、8205和127806。其中一个标量值可以适应单个UTF-16编码单元,但另外三个标量值较大,需要被拆分成两个编码单元。因此,总共有7个UTF-16编码单元组成该字符串,所以其长度为7。
以下是四个标量值在UTF-16编码单元中的分布情况:
- 128105 => 55357, 56425
- 127998 => 55356, 57342
- 8205 => 8205
- 127806 => 55356, 57150
总结
大多数JavaScript字符串操作也适用于UTF-16编码。然而,并非所有操作都是如此。例如,字符串切片(slice)操作也使用UTF-16编码单元。但并不是所有JavaScript字符串操作都是基于UTF-16编码的,比如对字符串进行迭代操作时,会基于标量值进行迭代。如果需要迭代扩展图形簇,可以使用 Intl.Segmenter() 对象,不过并不是所有浏览器都支持该功能。
const str = "farmer: 👩🏾🌾";
// Warning: this is not supported on all browsers!
const segments = new Intl.Segmenter().segment(str);
[...segments];
// => [
// { segment: "f", index: 0, input: "farmer: 👩🏾🌾" },
// { segment: "a", index: 1, input: "farmer: 👩🏾🌾" },
// { segment: "r", index: 2, input: "farmer: 👩🏾🌾" },
// { segment: "m", index: 3, input: "farmer: 👩🏾🌾" },
// { segment: "e", index: 4, input: "farmer: 👩🏾🌾" },
// { segment: "r", index: 5, input: "farmer: 👩🏾🌾" },
// { segment: ":", index: 6, input: "farmer: 👩🏾🌾" },
// { segment: " ", index: 7, input: "farmer: 👩🏾🌾" },
// { segment: "👩🏾🌾", index: 8, input: "farmer: 👩🏾🌾" }
// ]
想要深入了解这些复杂的内容,可以参考以下文章:["It's Not Wrong that "🤦🏼♂️".length == 7"](https://hsivonen.fi/string-length/) 和 ["JavaScript has a Unicode problem"](https://mathiasbynens.be/notes/javascript-unicode)。