Array.from与哪些字符分组?

41

我一直在研究JS,但不知道JS在使用Array.from()时如何决定要将哪些元素添加到创建的数组中。例如,下面的表情符号由两个代码点组成,因此其length为2,但Array.from()将这两个代码点视为一个,从而得到一个只有一个元素的数组:

const emoji = '';
console.log(Array.from(emoji)); // Output: [""]

然而,其他一些字符也有两个代码点,例如此字符षि(其 .length 也为2)。但是,Array.from 不会“分组”这个字符,而是产生两个元素:

const str = 'षि';
console.log(Array.from(str)); // Output: ["ष", "ि"]

我的问题是:当一个字符由两个代码点组成时,是什么决定了它是否被分解 (例如第二个示例) 或作为一个单独的元素处理 (例如第一个示例)?


5
请看UTF-16代理对... - Jonas Wilms
7
为什么表情符号的长度是2个字符?这是因为许多表情符号实际上由两个 Unicode 字符组成。第一个字符是基本表情符号,第二个字符是用于修改其外观的变化选择器。如何将包含表情符号的字符串拆分成数组?可以使用JavaScript中的正则表达式,并将Unicode代码点视为单个字符来切割字符串。如何在JavaScript中计算带有表情符号的字符串的正确长度?可以使用Array.from()方法将字符串转换为一个unicode数组,然后在此数组上使用.length属性即可获得字符串的正确长度。 - adiga
1
我对MDN的Array.from polyfill存在不同行为的问题感到担忧 :-s - Ele
1
@Ele 它只考虑具有“length”属性的对象。迭代器甚至Set都不能使用它。 - adiga
3个回答

28
Array.from 首先会尝试调用参数的迭代器(如果有的话),而字符串确实有迭代器,所以它会调用 String.prototype[Symbol.iterator],那么我们来看一下原型方法是如何工作的。在规范这里中有描述。
查找 CreateStringIterator 最终会导致你到达 21.1.5.2.1 %StringIteratorPrototype%.next ( ),其中执行以下操作:
第 9 步: 获取当前代码点 第 10 步: 创建一个包含从位置索引开始和由 cp.[[CodeUnitCount]] 连续的代码单元组成的字符串值 resultString。 第 11 步: 将 O.[[StringNextIndex]] 设置为 position + cp.[[CodeUnitCount]]。 第 12 步: 返回结果对象 CreateIterResultObject(resultString, false)。
你需要关注的是 CodeUnitCount。这个数字来自于 CodePointAt
第 3 步: 让 first 是字符串中位置索引处的代码单元。 第 4 步: 让 cp 是其数值等于 first 的代码点。 如果 first 不是前导代理项或尾随代理项,则返回记录 { [[CodePoint]]: cp, [[CodeUnitCount]]: 1, [[IsUnpairedSurrogate]]: false }。 如果 first 是尾随代理项或 position + 1 = size,则返回记录 { [[CodePoint]]: cp, [[CodeUnitCount]]: 1, [[IsUnpairedSurrogate]]: true }。 让 second 是字符串中位置索引为 position + 1 的代码单元。 如果 second 不是尾随代理项,则返回记录 { [[CodePoint]]: cp, [[CodeUnitCount]]: 1, [[IsUnpairedSurrogate]]: true }。 将 cp 设置为 ! UTF16DecodeSurrogatePair(first, second)。 返回记录 { [[CodePoint]]: cp, [[CodeUnitCount]]: 2, [[IsUnpairedSurrogate]]: false }
因此,使用 Array.from 迭代字符串时,仅当所讨论的字符是代理对的开头时,它才返回 CodeUnitCount 为 2。被解释为代理对的字符在 这里 中描述:

此类操作对于数值在包括范围 0xD800 到 0xDBFF 内的每个代码单元(由 Unicode 标准定义为前导代理或更正式地称为高代理代码单元)以及数值在包括范围 0xDC00 到 0xDFFF 内的每个代码单元(定义为尾随代理或更正式地称为低代理代码单元),应用以下规则..:

षि 不是代理对:

console.log('षि'.charCodeAt()); // First character code: 2359, or 0x937
console.log('षि'.charCodeAt(1)); // Second character code: 2367, or 0x93F

但是的字符是:

console.log(''.charCodeAt()); // 55357, or 0xD83D
console.log(''.charCodeAt(1)); // 56397, or 0xDC4D

''的第一个字符代码是D83D,以十六进制表示,它在前导代理范围内的0xD800到0xDBFF之间。相比之下,'षि'的第一个字符代码要低得多,并且不在该范围内。因此,'षि'被分开,但''没有。

षि由两个单独的字符组成:天城体字母Ssaि天城体元音符号I。当按照这个顺序挨在一起时,它们在视觉上合并为一个单独的字符,尽管它们由两个单独的字符组成。

相比之下,的字符代码仅在作为单个字形在一起时才有意义。如果你试图使用其中一个代码点的字符串而没有另一个,你将得到一个无意义的符号:

console.log(''[0]);
console.log(''[1]);


10
我认为,虽然这个回答基本上是正确的、有用的,并且提供了仔细的引用,但它没有清楚地解释这两种情况之间的关键区别:从Unicode的角度来看,षि实际上是由两个具有不同代码点的字符组合而成的单个字形(作为人类理解的一个抽象字符)。这与“”表情符号不同,后者本身就是一个完整的字符,即使它的代码点高到必须分割成代理对。我相信澄清这一点可以在很大程度上帮助这个(否则有价值的)回答。 - rhino
具体来说,辅音 ष (ṣ) 和元音 ि (i) 在图形上结合成音节 षि (ṣi)。 - Amadan
@CertainPerformance 在 "" 中只有一个代码点。这表明此答案中的术语可能不正确。 - Ben Aston

13

UTF-16(js中字符串使用的编码)使用16位单位。因此,每个可以使用15位表示的unicode被表示为一个代码点,而其他所有内容则被表示为两个,称为代理对。字符串的迭代器遍历代码点。

UTF-16维基百科


8
这里关键在于字符背后的编码。有些字符使用两个字节进行编码(UTF-16),Array.from会将其解释为两个字符。需要查看字符列表:

http://www.fileformat.info/info/charset/UTF-8/list.htm

http://www.fileformat.info/info/charset/UTF-16/list.htm

function displayHexUnicode(s) {
  console.log(s.split("").reduce((hex,c)=>hex+=c.charCodeAt(0).toString(16).padStart(4,"0"),""));
}

displayHexUnicode('षि');

console.log(Array.from('षि').forEach(x => displayHexUnicode(x)));


function displayHexUnicode(s) {
  console.log(s.split("").reduce((hex,c)=>hex+=c.charCodeAt(0).toString(16).padStart(4,"0"),""));
}

displayHexUnicode('');

console.log(Array.from('').forEach(x => displayHexUnicode(x)));


关于显示十六进制代码的函数:

Javascript: Unicode string to hex


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接