前几天我和JS一起玩,我看到了:
'é'.length;
// 1
'é'.length;
// 2, not the same output as the line before
'é'.split('').join('|');
// 'e|́'
(是的,所有这些都是有效的,您可以复制它们ð)
tl; dr
作为图像值得1000个字:
您可以使用Intl.Segmenter
const seg = new Intl.Segmenter('en', { granularity: "grapheme"});
[...seg.segment('🙌🏾')].length
// 1
[...seg.segment('é')].length
// 1
解释
本文将讨论字符 vs 代码单元 vs 代码point vs vs grapheme vs vs vs vs glyph 。。
定义
-
Character
:通用术语,可能意味着其他4个术语中的任何一个。 -
Code Unit
:代码单元是UTF-16编码中最小的数据单元。在UTF-16中,每个代码单元的大小为16位(2个字节)。它可以代表角色或完整字符的一部分,具体取决于字符的Unicode值。 -
Code Point
:代码点是分配给Unicode标准中特定字符的数值值。这是每个角色的唯一标识符,通常在十六进制中表示。例如,字母“ A”的代码点是u+0041。在UTF-16中,每个代码点由1或2代码单元组成。 -
Grapheme
:素数是具有含义并代表单个“用户感知”字符的写作系统的最小单元。在UTF-16中,每个素式由至少1个代码点组成。并非所有代码点都是图形的一部分,例如零宽度非邮轮。 -
Glyph
:字形是角色的视觉表示或图像。它是在屏幕或打印上出现的字符的实际形状或形式。一个字符可以具有与之相关的多个字形,代表不同的印刷变化或字体样式。
您可以检查https://stackoverflow.com/questions/27331819/whats-the-difference-between-a-character-a-code-point-a-glyph-and-a-grapheme以获取更多详细信息。
UTF-16
JavaScript使用UTF-16(而不是UTF-8,而不是其他许多语言。请注意:UTF-8也将遇到所有这些问题)。
在UTF-16中,字符在16位块中编码(代码单位)。例如,$
在十六进制中编码为0024
(因此,其符号U+0024
或'\u0024'
); €
编码为20AC
。
问题:使用16位代码单元只能导致65536个可能的字符,那么我们如何表示其他 targun ? UTF-16具有一个系统,它可以使用2 代码单位来编码一些代码点。例如,𐐷
是代码点 U+10437
将编码为D801 DC37
(高替代D801
和低替代DC37
)。
string.prototype.length
根据MDN,length
基于代码单位:
字符串值的长度数据属性包含UTF-16代码单元中字符串的长度。
这解释了为什么使用.length
的(u+1f64c)或ð·(u+10437),因为它们以2个代码为单位进行编码:
'𐐷'.length; // U+10437
// 2
'🙌'.length; // U+1F64C
// 2
这种情况的一种可能的修复是使用iterators
。根据MDN的研究,迭代器再次工作(他们说角色,但表示代码点):
由于长度计数代码单元而不是字符,因此,如果要获得字符的数量,则可以首先用其迭代器将字符串拆分,该迭代均通过字符
迭代
确实有效
[...'𐐷'].length // U+10437
// 1
[...'🙌'].length // U+1F64C
// 1
[...'é'].length
// 2
[...'🙌🏾']
// 2
,但不是所有字符。为什么?
Unicode组成
unicode的另一个特异性是它可以结合多个代码点形成素数。这称为规范等价
(请参阅https://unicode.org/reports/tr15/#Canon_Compat_Equivalence)。
例如,字母“”可以是此字符的代码点,也可以是“ c”的代码点,然后是ducritic mark“⧔§“
我们还可以使用归一化NFD和NFC在预先构造和分解形式之间切换(请参阅https://unicode.org/reports/tr15/#Norm_Forms):
许多字符称为规范复合材料或预先构成的字符。在D形式中,它们被分解了。在C形式中,它们通常是
这解释了为什么é
的长度在最初的示例中为1或2:
- 分解表格2代码点
- 预先构造的表格 - 1代码点
在JavaScript中,您可以使用String.prototype.normalize
(MDN):
'é'.length;
// 1
'é'.normalize('NFD').length;
// 2
'é'.normalize('NFD').normalize('NFC').length;
// 1
表情符号序列
与角色组成类似,表情符号可以与特殊字符结合在一起(这不是详尽的列表):
-
肤色修饰符可用于自定义表情符号的颜色皮肤
例如,“ð” +“ð” +“ð¾”(Medium-Dark Skin Tone modifier)
组成[...'🙌🏾']; // ['🙌', '🏾']
-
零宽的木匠(zwj)可用于将某些表情符号合并在一起
例如,“ð®”由“ð®” +“”(ZWJ) +“ð”
组成 and“ð©ð©ð§âð€”由每个单独的家庭成员加上ZWJS组成:
[...'👩👩👧👦']; // ['👩', '', '👩', '', '👧', '', '👦']
-
变体选择器可以用于选择代码点的其他字形变体
例如,“âKish”由“â” +“!”组成(Variation Selector-16将显示为表情符号)
Intl.Segments
2021年,TC39委员会添加到Ecmascript Intl.Segmenter:
intl.smentementer对象启用对区域敏感的文本分割,使您能够从字符串中获取有意义的项目(绘制,单词,单词或句子)。
选择了一个语言环境后,您可以使用.segment
使用字符串的每个素器生成迭代器:
const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
for (const grapheme of seg.segment('Hélô 👩👩👧👦 🙌🏾')) {
console.log(grapheme.segment);
}
// "H"
// "é"
// "l"
// "ô"
// " "
// "👩👩👧👦"
// " "
// "🙌🏾"
,如果要获取chupereme的数量(例如.length
),则可以首先将其转换为数组:
[...seg.segment('🙌🏾')].length;
// 1
[...seg.segment('é')].length;
// 1
浏览器兼容性
可悲的是,在此日期,Firefox的写作日期不支持caniuse.com。如果您想遵循其开发,则可以跟踪this issue。