RegExp v 标志,使用集合符号和字符串属性

发布时间 · 标签:ECMAScript

JavaScript 从 ECMAScript 3(1999 年)开始就支持正则表达式。十六年后,ES2015 引入了 Unicode 模式(u 标志)粘性模式(y 标志)RegExp.prototype.flags 获取器。三年后,ES2018 引入了 dotAll 模式(s 标志)后顾断言命名捕获组Unicode 字符属性转义。在 ES2020 中,String.prototype.matchAll 使得使用正则表达式变得更加容易。JavaScript 正则表达式已经走过了很长的路,并且还在不断改进。

最新的例子是 新的 unicodeSets 模式,使用 v 标志启用。这种新模式解锁了对扩展字符类的支持,包括以下功能

本文将深入探讨这些内容。但首先,让我们看看如何使用这个新标志

const re = //v;

v 标志可以与现有的正则表达式标志组合使用,但有一个显著的例外。v 标志启用了 u 标志的所有优点,但还增加了额外的功能和改进——其中一些与 u 标志不兼容。重要的是,v 是一个与 u 完全独立的模式,而不是一个补充模式。因此,vu 标志不能组合使用——尝试在同一个正则表达式上使用这两个标志会导致错误。唯一有效的选项是:要么使用 u,要么使用 v,要么两者都不使用。但由于 v 是功能最全面的选项,所以这个选择很容易做出…

让我们深入了解一下新功能!

字符串的 Unicode 属性 #

Unicode 标准为每个符号分配了各种属性和属性值。例如,要获取希腊字母表中使用的符号集,请在 Unicode 数据库中搜索其 Script_Extensions 属性值包含 Greek 的符号。

ES2018 Unicode 字符属性转义使在 ECMAScript 正则表达式中本地访问这些 Unicode 字符属性成为可能。例如,模式 \p{Script_Extensions=Greek} 匹配所有在希腊字母表中使用的符号

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

根据定义,Unicode 字符属性扩展到一组代码点,因此可以将其转译为包含它们分别匹配的代码点的字符类。例如,\p{ASCII_Hex_Digit} 等效于 [0-9A-Fa-f]:它一次只匹配一个 Unicode 字符/代码点。在某些情况下,这还不够

// Unicode defines a character property named “Emoji”.
const re = /^\p{Emoji}$/u;

// Match an emoji that consists of just 1 code point:
re.test('⚽'); // '\u26BD'
// → true ✅

// Match an emoji that consists of multiple code points:
re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → false ❌

在上面的例子中,正则表达式不匹配 👨🏾‍⚕️ 表情符号,因为它恰好包含多个代码点,而 Emoji 是一个 Unicode 字符 属性。

幸运的是,Unicode 标准还定义了几个 字符串属性。这些属性扩展到一组字符串,每个字符串包含一个或多个代码点。在正则表达式中,字符串属性转换为一组备选方案。为了说明这一点,想象一个 Unicode 属性,它适用于字符串 'a''b''c''W''xy''xyz'。此属性转换为以下正则表达式模式之一(使用备选方案):xyz|xy|a|b|c|Wxyz|xy|[a-cW]。(最长的字符串放在最前面,这样前缀如 'xy' 不会隐藏更长的字符串如 'xyz'。)与现有的 Unicode 属性转义不同,此模式可以匹配多字符字符串。以下是一个使用字符串属性的示例

const re = /^\p{RGI_Emoji}$/v;

// Match an emoji that consists of just 1 code point:
re.test('⚽'); // '\u26BD'
// → true ✅

// Match an emoji that consists of multiple code points:
re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ✅

此代码片段引用了字符串属性 RGI_Emoji,Unicode 将其定义为“所有有效表情符号(字符和序列)的子集,推荐用于一般交换”。有了它,我们现在可以匹配表情符号,无论它们在底层包含多少个代码点!

v 标志从一开始就支持以下 Unicode 字符串属性

  • Basic_Emoji
  • Emoji_Keycap_Sequence
  • RGI_Emoji_Modifier_Sequence
  • RGI_Emoji_Flag_Sequence
  • RGI_Emoji_Tag_Sequence
  • RGI_Emoji_ZWJ_Sequence
  • RGI_Emoji

随着 Unicode 标准定义更多字符串属性,支持的属性列表可能会在未来增长。虽然所有当前的字符串属性都与表情符号相关,但未来的字符串属性可能会服务于完全不同的用例。

注意:虽然字符串属性目前受限于新的 v 标志,但 我们计划最终在 u 模式下也提供它们

集合符号 + 字符串字面量语法 #

在使用 \p{…} 转义(无论是字符属性还是新的字符串属性)时,执行差集/减法或交集可能很有用。使用 v 标志,字符类现在可以嵌套,并且这些集合操作现在可以在字符类内部执行,而不是使用相邻的前瞻或后顾断言或表达计算范围的冗长字符类。

使用 -- 进行差集/减法 #

语法 A--B 可用于匹配A 中但不在 B的字符串,即差集/减法。

例如,如果你想匹配所有希腊符号,除了字母 π?使用集合符号,解决这个问题很简单

/[\p{Script_Extensions=Greek}--π]/v.test('π'); // → false

通过使用 -- 进行差集/减法,正则表达式引擎会为你完成繁重的工作,同时保持代码的可读性和可维护性。

如果我们想减去字符集 αβγ,而不是单个字符,该怎么办?没问题——我们可以使用嵌套的字符类并减去它的内容

/[\p{Script_Extensions=Greek}--[αβγ]]/v.test('α'); // → false
/[\p{Script_Extensions=Greek}--[α-γ]]/v.test('β'); // → false

另一个例子是匹配非 ASCII 数字,例如为了稍后将它们转换为 ASCII 数字

/[\p{Decimal_Number}--[0-9]]/v.test('𑜹'); // → true
/[\p{Decimal_Number}--[0-9]]/v.test('4'); // → false

集合符号也可以与新的字符串属性一起使用

// Note: 🏴󠁧󠁢󠁳󠁣󠁴󠁿 consists of 7 code points.

/^\p{RGI_Emoji_Tag_Sequence}$/v.test('🏴󠁧󠁢󠁳󠁣󠁴󠁿'); // → true
/^[\p{RGI_Emoji_Tag_Sequence}--\q{🏴󠁧󠁢󠁳󠁣󠁴󠁿}]$/v.test('🏴󠁧󠁢󠁳󠁣󠁴󠁿'); // → false

此示例匹配任何 RGI 表情符号标签序列,除了苏格兰国旗。请注意 \q{…} 的使用,这是字符类中字符串字面量的另一个新语法。例如,\q{a|bc|def} 匹配字符串 abcdef。如果没有 \q{…},将无法减去硬编码的多字符字符串。

使用 && 进行交集 #

A&&B 语法匹配同时在 AB的字符串,即交集。这使你可以执行诸如匹配希腊字母之类的操作

const re = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;
// U+03C0 GREEK SMALL LETTER PI
re.test('π'); // → true
// U+1018A GREEK ZERO SIGN
re.test('𐆊'); // → false

匹配所有 ASCII 空格

const re = /[\p{White_Space}&&\p{ASCII}]/v;
re.test('\n'); // → true
re.test('\u2028'); // → false

或者匹配所有蒙古数字

const re = /[\p{Script_Extensions=Mongolian}&&\p{Number}]/v;
// U+1817 MONGOLIAN DIGIT SEVEN
re.test('᠗'); // → true
// U+1834 MONGOLIAN LETTER CHA
re.test('ᠴ'); // → false

并集 #

匹配在 A 或 B 中的字符串以前已经可以使用类似 [\p{Letter}\p{Number}] 的字符类来实现,用于单个字符字符串。使用 v 标志,此功能变得更加强大,因为它现在可以与字符串属性或字符串字面量一起使用

const re = /^[\p{Emoji_Keycap_Sequence}\p{ASCII}\q{🇧🇪|abc}xyz0-9]$/v;

re.test('4️⃣'); // → true
re.test('_'); // → true
re.test('🇧🇪'); // → true
re.test('abc'); // → true
re.test('x'); // → true
re.test('4'); // → true

此模式中的字符类组合了

  • 一个字符串属性 (\p{Emoji_Keycap_Sequence})
  • 一个字符属性 (\p{ASCII})
  • 用于多代码点字符串 🇧🇪abc 的字符串字面量语法
  • 用于单个字符 xyz 的经典字符类语法
  • 用于从 09 的字符范围的经典字符类语法

另一个例子是匹配所有常用的国旗表情符号,无论它们是编码为两个字母的 ISO 代码 (RGI_Emoji_Flag_Sequence) 还是编码为特殊情况的标签序列 (RGI_Emoji_Tag_Sequence)

const reFlag = /[\p{RGI_Emoji_Flag_Sequence}\p{RGI_Emoji_Tag_Sequence}]/v;
// A flag sequence, consisting of 2 code points (flag of Belgium):
reFlag.test('🇧🇪'); // → true
// A tag sequence, consisting of 7 code points (flag of England):
reFlag.test('🏴󠁧󠁢󠁥󠁮󠁧󠁿'); // → true
// A flag sequence, consisting of 2 code points (flag of Switzerland):
reFlag.test('🇨🇭'); // → true
// A tag sequence, consisting of 7 code points (flag of Wales):
reFlag.test('🏴󠁧󠁢󠁷󠁬󠁳󠁿'); // → true

改进的区分大小写匹配 #

ES2015 u 标志存在 令人困惑的区分大小写匹配行为。考虑以下两个正则表达式

const re1 = /\p{Lowercase_Letter}/giu;
const re2 = /[^\P{Lowercase_Letter}]/giu;

第一个模式匹配所有小写字母。第二个模式使用 \P 而不是 \p 来匹配除了小写字母之外的所有字符,但随后被包装在一个否定字符类 ([^…]) 中。通过设置 i 标志 (ignoreCase),这两个正则表达式都变得不区分大小写。

直观地,你可能期望这两个正则表达式表现相同。实际上,它们的行为非常不同

const re1 = /\p{Lowercase_Letter}/giu;
const re2 = /[^\P{Lowercase_Letter}]/giu;

const string = 'aAbBcC4#';

string.replaceAll(re1, 'X');
// → 'XXXXXX4#'

string.replaceAll(re2, 'X');
// → 'aAbBcC4#''

新的 v 标志的行为不那么令人意外。使用 v 标志而不是 u 标志,这两个模式的行为相同

const re1 = /\p{Lowercase_Letter}/giv;
const re2 = /[^\P{Lowercase_Letter}]/giv;

const string = 'aAbBcC4#';

string.replaceAll(re1, 'X');
// → 'XXXXXX4#'

string.replaceAll(re2, 'X');
// → 'XXXXXX4#'

更一般地说,v 标志使 [^\p{X}][\P{X}]\P{X}[^\P{X}][\p{X}]\p{X},无论 i 标志是否设置。

进一步阅读 #

提案仓库 包含有关这些功能及其设计决策的更多详细信息和背景。

作为我们对这些 JavaScript 功能工作的一部分,我们超越了“仅仅”提出对 ECMAScript 的规范更改。我们将“字符串属性”的定义上游到 Unicode UTS#18,以便其他编程语言能够以统一的方式实现类似的功能。我们也 建议对 HTML 标准进行更改,目的是在 pattern 属性中也启用这些新功能。

RegExp v 标志支持 #

V8 v11.0(Chrome 110)通过 --harmony-regexp-unicode-sets 标志提供了对这种新功能的实验性支持。V8 v12.0(Chrome 112)默认启用了新功能。Babel 也支持转译 v 标志——在 Babel REPL 中尝试本文中的示例!下面的支持表链接到你可以订阅以获取更新的跟踪问题。