intl.segmenter():不要使用string.split()或string.length
#javascript #intl #encoding
  1. TL;DR
  2. Explanation
    1. Definitions
    2. UTF-16
    3. String.prototype.length
    4. Unicode composition
    5. Emoji Sequence
  3. Intl.Segmenter
    1. Browser compatibility

前几天我和JS一起玩,我看到了:

'é'.length;
// 1
''.length;
// 2, not the same output as the line before
''.split('').join('|');
// 'e|́'

(是的,所有这些都是有效的,您可以复制它们ð)

tl; dr

作为图像值得1000个字:

Grapheme vs code unit vs code point

您可以使用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)。

$, €, and 𐐷 encoded in UTF-16 in code units

string.prototype.length

根据MDNlength基于代码单位

字符串值的长度数据属性包含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“⧔§“

Ç <-> C+◌̧

我们还可以使用归一化NFD和NFC在预先构造和分解形式之间切换(请参阅https://unicode.org/reports/tr15/#Norm_Forms):

许多字符称为规范复合材料或预先构成的字符。在D形式中,它们被分解了。在C形式中,它们通常是

"Å" can be decomposed into "A+̊ " or precomposed as "Å"

这解释了为什么é的长度在最初的示例中为1或2:

  • 分解表格2代码点
  • 预先构造的表格 - 1代码点

在JavaScript中,您可以使用String.prototype.normalizeMDN):

'é'.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

Browser compatibility table