For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/year/2018/proposal-regexp-unicode-property-escapes.md.
  • 简体中文
  • RegExp Unicode Property Escapes S4

    中文标题:正则表达式中的 Unicode 属性转义

    提案概览
    提案速览

    该提案为 JavaScript 正则表达式添加了形式为 \p{…} 和 \P{…} 的 Unicode 属性转义,允许开发人员基于 Unicode 属性(如 Script、General_Category 和二进制属性)匹配字符。它消除了对运行时库或构建脚本的需求,提高了性能,并使数据随 Unicode 标准自动更新。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    ECMAScript 提案:正则表达式中的 Unicode 属性转义

    状态

    本提案处于 TC39 流程的第 4 阶段,并计划纳入 ES2018。

    动机

    Unicode 标准为每个符号分配了各种属性和属性值。例如,要获取仅在希腊文字中使用的符号集,请在 Unicode 数据库中搜索 Script 属性设置为 Greek 的符号。

    目前 ECMAScript 正则表达式无法原生访问这些 Unicode 字符属性。这使得开发人员在正则表达式中难以支持完整的 Unicode。当前有两种选项,但都不理想:

    1. 使用诸如 XRegExp 之类的库在运行时创建正则表达式:

      const regexGreekSymbol = XRegExp('\\p{Greek}', 'A');
      regexGreekSymbol.test('π');
      // → true

      这种方法的缺点是 XRegExp 库是运行时依赖,可能不适合对性能敏感的应用程序。对于 Web 使用,还有额外的加载时间性能损失:xregexp-all-min.js.gz 在压缩和 gzip 压缩后占用超过 35 KB 的空间。每当 Unicode 标准更新时,必须发布新版本的 XRegExp,最终用户需要更新其 XRegExp 副本才能使用最新的可用数据。

    2. 使用诸如 Regenerate 之类的库在构建时生成正则表达式:

      const regenerate = require('regenerate');
      const codePoints = require('unicode-9.0.0/Script/Greek/code-points.js');
      const set = regenerate(codePoints);
      set.toString();
      // → '[\u0370-\u0373\u0375-\u0377\u037A-\u037D\u037F\u0384\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03E1\u03F0-\u03FF\u1D26-\u1D2A\u1D5D-\u1D61\u1D66-\u1D6A\u1DBF\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FC4\u1FC6-\u1FD3\u1FD6-\u1FDB\u1FDD-\u1FEF\u1FF2-\u1FF4\u1FF6-\u1FFE\u2126\uAB65]|\uD800[\uDD40-\uDD8E\uDDA0]|\uD834[\uDE00-\uDE45]'
      // 想象这里有更多代码来将此模式保存到文件。

      这种方法可以实现最佳的运行时性能,但生成的正则表达式往往相当大(可能导致 Web 上的加载时间性能问题)。最大的缺点是它需要构建脚本,随着开发人员需要更多 Unicode 感知的正则表达式,这会变得繁琐。每当 Unicode 标准更新时,必须更新构建脚本并部署其结果才能使用最新的可用数据。

    提议的解决方案

    我们提议添加形式为 \p{…}\P{…}Unicode 属性转义。Unicode 属性转义是正则表达式中可用的一种新型转义序列,这些正则表达式具有 u 标志。使用此功能,上述正则表达式可以写成:

    const regexGreekSymbol = /\p{Script=Greek}/u;
    regexGreekSymbol.test('π');
    // → true

    本提案解决了上述所有问题:

    • 创建 Unicode 感知的正则表达式不再繁琐。
    • 不依赖运行时库。
    • 正则表达式模式紧凑且可读——不再有文件大小膨胀。
    • 不再需要创建在构建时生成正则表达式的脚本。
    • 从开发人员的角度来看,使用 Unicode 属性转义的代码“自动”保持最新:每当 Unicode 标准更新时,ECMAScript 引擎都会更新其数据。

    高级 API

    非二进制的 Unicode 属性的 Unicode 属性转义如下所示:

    \p{UnicodePropertyName=UnicodePropertyValue}

    可以使用 PropertyAliases.txtPropertyValueAliases.txt 中定义的别名代替规范属性和值名称。使用未知的属性名称或值会触发早期的 SyntaxError

    对于二进制属性,可使用以下语法:

    \p{LoneUnicodePropertyNameOrValue}

    此语法也可以用作 General_Category 值的简写,例如 \p{Letter} 代替 \p{General_Category=Letter}

    \P{…}\p{…} 的否定形式。

    实现必须支持规范提案中提到的 Unicode 属性列表及其属性别名。这包括 General_CategoryScriptScript_Extensions 以及一些二进制属性(包括但不限于 AlphabeticUppercaseLowercaseWhite_SpaceNoncharacter_Code_PointDefault_Ignorable_Code_PointAnyASCIIAssignedID_StartID_ContinueJoin_ControlEmoji_PresentationEmoji_ModifierEmoji_Modifier_Base 等)。这是 UTS18 RL1.2 要求的超集。为确保互操作性,实现不得将 Unicode 属性支持扩展到其余属性。

    常见问题解答

    向后兼容性如何?

    在没有 u 标志的正则表达式中,模式 \pp 的(不必要的)转义序列。形式为 \p{Letter} 的模式可能已经出现在没有 u 标志的现有正则表达式中,因此我们无法在不破坏向后兼容性的情况下为这些模式赋予新的含义。

    因此,ECMAScript 2015 使像 \p\P 这样不必要的转义序列在设置 u 标志时抛出异常。这使我们能够在不破坏向后兼容性的情况下,在带有 u 标志的正则表达式中更改 \p{…}\P{…} 的含义。

    为什么不支持松散匹配?

    UAX44-LM3 指定了用于比较 Unicode 属性和值别名的松散匹配规则。

    忽略大小写、空格、下划线、连字符、[…]

    松散匹配使 \p{lB=Ba} 等价于 \p{Line_Break=Break_After}/\p{___lower C-A-S-E___}/u 等价于 /\p{Lowercase}/u。我们断言此功能不会增加任何价值,实际上会损害代码的可读性和可维护性。

    如果有需要,可以稍后作为单独的 ECMAScript 提案添加对松散匹配的支持。但是,如果我们现在添加,就没有回头路了。

    为什么不支持 is 前缀?

    UAX44-LM3 指定了用于比较 Unicode 属性和值别名的松散匹配规则,其中之一是:

    忽略 […] 任何初始前缀字符串 is

    此规则使 Script=IsGreekIsScript=Greek 等价于 Script=Greek。我们断言此功能不会增加任何价值,实际上会损害代码可读性。它引入了歧义并增加了实现复杂性,因为某些属性值或别名已经以 is 开头,例如 Decomposition_Type=IsolatedLine_Break=IS(它是 Line_Break=Infix_Numeric 的别名)。

    与其他语言中的 Unicode 属性转义的兼容性也不是论据,因为似乎没有一个现有的正则表达式引擎完全按照 UAX44-LM3 中的描述实现 is 前缀,而那些部分实现的引擎行为也大相径庭。

    宁愿严格也不愿含糊。

    如果有需要,可以稍后作为单独的 ECMAScript 提案添加对 is 前缀的支持。但是,如果我们现在添加,就没有回头路了。

    为什么不支持例如 \pL 作为 \p{L} 的简写?

    这种简写不会增加任何价值,因此所增加的实现复杂性(即使很小)也不值得。\p{L} 有效;除了与其他语言兼容之外,没有理由为它引入另一种语法,而无论如何这只是一个乌托邦式的目标。

    如果有需要,可以稍后作为单独的 ECMAScript 提案添加对此简写的支持。但是,如果我们现在添加,就没有回头路了。

    为什么使用 =(而不是其他)作为分隔符?

    \p{…=…} 中的 =(?=…) 中的 =(正向先行断言)和 (?<=…) 中的 =(正向后行断言)保持一致。此外,= 是大多数正则表达式引擎使用的分隔符。有关更多信息,请参阅问题 #8。

    为什么除了 = 之外不支持 : 作为分隔符?

    支持多个分隔符不会增加任何价值,因此所增加的实现复杂性(即使很小)也不值得。\p{Script_Extensions=Greek} 有效;除了与其他语言兼容之外,没有理由为它引入另一种语法,而无论如何这只是一个乌托邦式的目标。

    如果有需要,可以稍后作为单独的 ECMAScript 提案添加对 : 分隔符的支持。但是,如果我们现在添加,就没有回头路了。

    为什么不支持例如 \p{ScriptName} 作为 \p{Script=ScriptName} 的简写?

    在大多数使用情况下,应使用 Script_Extensions 而不是 ScriptUTS24 通过实际示例很好地解释了这一点。因此,为 Script_Extensions 添加简写比 为 Script 添加简写更有意义。然而,这样做会引起混淆,因为这两个属性的值集是相同的。例如,不清楚 \p{Old_Persian} 指的是 Script 还是 Script_Extensions

    为什么不重载 \u{…} 而不是添加 \p{…}\P{…}

    支持重载 \u{…} 的主要论点是它暗示了 Unicode。我们断言这种提示是不必要的,因为正则表达式上必需的 u 标志已经表明了 Unicode。

    \p{…} 中的 p 代表“属性”。结合 u 标志,这很好地表明花括号内的表达式与 Unicode 属性相关。

    重载 \u{…} 会引入歧义。想象一下,一个新的二进制属性或通用类别 Beef 被添加到 Unicode 标准中。由于 Beef 仅由十六进制数字组成([A-Fa-f0-9]),因此不清楚 \u{Beef}U+BEEF HANGUL SYLLABLE BBEGS 的码点转义序列还是引用名为 Beef 的属性/类别的属性转义序列。

    其他支持 Unicode 属性转义的现有语言使用 \p{…}\P{…}。尽管与其他实现兼容不是目标(因为它们之间本就不兼容),但遵循传统并重用开发人员已经熟悉的基本语法是有意义的。

    为什么不支持 Name 属性(\p{Name=…})?

    开发人员已经有一种方法可以在不将特定符号放入源代码的情况下引用它:\u{1D306} 形式的 Unicode 码点转义。因此,支持 \p{Name=TETRAGRAM FOR CENTRE} 的需求不足以证明将其纳入本提案是合理的。

    可以稍后作为单独的 ECMAScript 提案添加对 Name 属性的支持。但是,如果我们现在添加,就没有回头路了。

    示例

    \d 的 Unicode 感知版本

    要匹配 Unicode 中的任何十进制数字,而不只是 ASCII [0-9],请使用 \p{Decimal_Number} 而不是 \d,如 UTS18 所述。

    const regex = /^\p{Decimal_Number}+$/u;
    regex.test('𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼');
    // → true

    \D 的 Unicode 感知版本

    要匹配任何不是十进制数字的 Unicode 符号,而不只是 [^0-9],请使用 \P{Decimal_Number} 而不是 \D

    const regex = /^\P{Decimal_Number}+$/u;
    regex.test('Իմ օդաթիռը լի է օձաձկերով');
    // → true

    \w 的 Unicode 感知版本

    要匹配 Unicode 中的任何单词符号,而不只是 ASCII [a-zA-Z0-9_],请使用 [\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}],如 UTS18 所述。

    const regex = /([\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]+)/gu;
    const text = `
    Amharic: የኔ ማንዣበቢያ መኪና በዓሣዎች ተሞልቷል
    Bengali: আমার হভারক্রাফ্ট কুঁচে মাছ-এ ভরা হয়ে গেছে
    Georgian: ჩემი ხომალდი საჰაერო ბალიშზე სავსეა გველთევზებით
    Macedonian: Моето летачко возило е полно со јагули
    Vietnamese: Tàu cánh ngầm của tôi đầy lươn
    `;
    
    let match;
    while (match = regex.exec(text)) {
      const word = match[1];
      console.log(`Matched word with length ${ word.length }: ${ word }`);
    }

    控制台输出:

    Matched word with length 7: Amharic
    Matched word with length 2: የኔ
    Matched word with length 6: ማንዣበቢያ
    Matched word with length 3: መኪና
    Matched word with length 5: በዓሣዎች
    Matched word with length 5: ተሞልቷል
    Matched word with length 7: Bengali
    Matched word with length 4: আমার
    Matched word with length 11: হভারক্রাফ্ট
    Matched word with length 5: কুঁচে
    Matched word with length 3: মাছ
    Matched word with length 1: এ
    Matched word with length 3: ভরা
    Matched word with length 3: হয়ে
    Matched word with length 4: গেছে
    Matched word with length 8: Georgian
    Matched word with length 4: ჩემი
    Matched word with length 7: ხომალდი
    Matched word with length 7: საჰაერო
    Matched word with length 7: ბალიშზე
    Matched word with length 6: სავსეა
    Matched word with length 12: გველთევზებით
    Matched word with length 10: Macedonian
    Matched word with length 5: Моето
    Matched word with length 7: летачко
    Matched word with length 6: возило
    Matched word with length 1: е
    Matched word with length 5: полно
    Matched word with length 2: со
    Matched word with length 6: јагули
    Matched word with length 10: Vietnamese
    Matched word with length 3: Tàu
    Matched word with length 4: cánh
    Matched word with length 4: ngầm
    Matched word with length 3: của
    Matched word with length 3: tôi
    Matched word with length 3: đầy
    Matched word with length 4: lươn

    \W 的 Unicode 感知版本

    要匹配 Unicode 中的任何非单词符号,而不只是 [^a-zA-Z0-9_],请使用 [^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

    匹配表情符号

    要匹配表情符号,UTR51 中的二进制属性会派上用场。

    const regex = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;

    此正则表达式从左到右匹配:

    1. 带可选修饰符的表情符号(\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?);
    2. 任何其余默认呈现为表情符号而非文本的符号(\p{Emoji_Presentation});
    3. 默认呈现为文本但使用 U+FE0F 变体选择器-16 强制呈现为表情符号的符号(\p{Emoji}\uFE0F)。
    const regex = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
    const text = `
    \u{231A}: ⌚ default emoji presentation character (Emoji_Presentation)
    \u{2194}\u{FE0F}: ↔️ default text presentation character rendered as emoji
    \u{1F469}: 👩 emoji modifier base (Emoji_Modifier_Base)
    \u{1F469}\u{1F3FF}: 👩🏿 emoji modifier base followed by a modifier
    `;
    
    let match;
    while (match = regex.exec(text)) {
      const emoji = match[0];
      console.log(`Matched sequence ${ emoji } — code points: ${ [...emoji].length }`);
    }

    控制台输出:

    Matched sequence ⌚ — code points: 1
    Matched sequence ⌚ — code points: 1
    Matched sequence ↔️ — code points: 2
    Matched sequence ↔️ — code points: 2
    Matched sequence 👩 — code points: 1
    Matched sequence 👩 — code points: 1
    Matched sequence 👩🏿 — code points: 2
    Matched sequence 👩🏿 — code points: 2

    其他示例

    匹配 Unicode 中的任何数字符号,包括非十进制符号,如罗马数字:

    const regex = /^\p{Number}+$/u;
    regex.test('²³¹¼½¾𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼㉛㉜㉝ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ');
    // → true

    匹配 ECMAScript IdentifierStartIdentifierPart 符号而无需构建脚本生成的复杂正则表达式

    const regexIdentifierStart = /[$_\p{ID_Start}]/u;
    const regexIdentifierPart = /[$\u200C\u200D\p{ID_Continue}]/u;
    const regexIdentifierName = /^(?:[$_\p{ID_Start}])(?:[$\u200C\u200D\p{ID_Continue}])*$/u;

    规范

    实现