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
完全独立的模式,而不是一个补充模式。因此,v
和 u
标志不能组合使用——尝试在同一个正则表达式上使用这两个标志会导致错误。唯一有效的选项是:要么使用 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|W
或 xyz|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}
匹配字符串 a
、bc
和 def
。如果没有 \q{…}
,将无法减去硬编码的多字符字符串。
使用 &&
进行交集 #
A&&B
语法匹配同时在 A
和 B
中的字符串,即交集。这使你可以执行诸如匹配希腊字母之类的操作
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
的字符串字面量语法 - 用于单个字符
x
、y
和z
的经典字符类语法 - 用于从
0
到9
的字符范围的经典字符类语法
另一个例子是匹配所有常用的国旗表情符号,无论它们是编码为两个字母的 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 中尝试本文中的示例!下面的支持表链接到你可以订阅以获取更新的跟踪问题。