大家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
今天给大家带来的主题是 JavaScript 基础中的 RegExp,重点在于介绍 RegExp 最新支持的标志位 v。话不多说,直接进入正题!
1.RegExp 的不同标志位支持
JavaScript 自 ECMAScript 3 (1999) 起就支持正则表达式。 十六年后,ES2015 引入了 Unicode 模式(u 标志)、sticky 模式(y 标志)和 RegExp.prototype.flags 属性。 又过了三年,ES2018 引入了 dotAll 模式(s 标志)、lookbehind 断言、命名捕获组和 Unicode 字符属性转义。 在 ES2020 中,String.prototype.matchAll 使开发者使用正则表达式变得更加容易。 JavaScript 正则表达式已经取得了长足的进步,并且仍在不断改进。
接下来一起简单看下JavaScript 中 RegExp的不同标志位的使用。
u 标志
对于 u 标志位,可以通过它启用各种与 Unicode 相关的功能。
// 注意:“a”是 U+0061 拉丁文小写字母 A,一个 BMP 符号。
console.log(/\u{61}/u.test("a"));
// 返回 true
// Note: `` is U+1D306 TETRAGRAM FOR CENTRE, an astral symbol.
console.log(/\u{1D306}/u.test(""));
// 返回 true
y 标志
对于 y 标志符,RegExp 实例的 sticky 访问器属性返回是否与此正则表达式一起使用 y 标志。
const str1 = "table football";
const regex1 = new RegExp("foo", "y");
regex1.lastIndex = 6;
console.log(regex1.sticky);
// 如果有 y 标志,则sticky值为 true,否则为false。
// y 标志指示正则表达式仅尝试从 lastIndex 属性指示的索引匹配目标字符串
// 与全局正则表达式不同,不会尝试从任何后续索引匹配
// 输出: true
console.log(regex1.test(str1));
// 返回 true
console.log(regex1.test(str1));
// 返回 false
flags 属性
正则表达式的 flags 属性返回一个字符串, flags 属性中的标志按字母顺序排序(从左到右,例如“dgimsuvy”)。 它实际上一一调用其他标志访问器(hasIndices、global 等)并连接结果。
// 按字母顺序输出 RegExp 标志
console.log(/foo/gi.flags);
// Expected output: "gi"
console.log(/bar/muy.flags);
// Expected output: "muy"
正则表达式的 s 标志
默认情况下, .符号匹配除行终止符之外的任何字符:
/foo.bar/u.test("foo\nbar");
// → false
ES2018 引入了 dotAll 模式,通过 s 标志启用。在 dotAll 模式下,.也匹配行终止符。
/foo.bar/su.test("foo\nbar");
// → true
ECMAScript 目前支持向前执行此操作的前瞻断言(lookahead assertions)。正向先行(Positive lookahead )确保一个模式后面跟着另一个模式:
const pattern = /\d+(?= dollars)/u;
// ?=子模式 是前瞻断言的语法
const result = pattern.exec("42 dollars");
// → result[0] === '42'
负向先行(Negative lookahead)可确保一个模式后面不会跟随另一个模式:
const pattern = /\d+(?! dollars)/u;
// ?!子模式,这个是Negative lookahead模式的语法
const result = pattern.exec("42 pesos");
// → result[0] === '42'
ES2018 添加了对后行断言的支持。positive lookbehind 确保一个模式之前有另一个模式:
const pattern = /(?<=\$)\d+/u;
// positive lookbehind 用来判断一个字符串前面为某种模式的字符串
// 语法为:(?<=子模式)
const result = pattern.exec("$42");
// → result[0] === '42'
negative lookbehind 可确保一个模式前面不会有另一个模式:
const pattern = /(?<!\$)\d+/u;
// negative lookbehind是用来判断一个字符串前面不为某个模式的字符串
// 语法为:(?<!子模式)
const result = pattern.exec("€42");
// → result[0] === '42'
同时 ES2018 添加了对命名捕获组的支持,从而使代码更具可读性和可维护性。
const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
const result = pattern.exec("2017-01-25");
// → result.groups.year === '2017'
// → result.groups.month === '01'
// → result.groups.day === '25'
更多内容可以参考文末的资料,这里不再继续展开。
2.正则表达式的 v 标志
正则表达式最新的例子是新的 unicodeSets 模式,使用 v 标志启用。 这种新模式解锁了对扩展字符类的支持,包括以下功能:
- 字符串的 Unicode 属性
- 集合表示法+字符串文字语法
- 改进的不区分大小写的匹配
下面是使用 v 标志的一个示例:
const re = /…/v;
v 标志可以与现有的正则表达式标志结合使用,但有一个例外。 v 标志启用了 u 标志的所有优点,但还具有其他功能和改进,其中一些功能与 u 标志向后不兼容。
至关重要的是,v 是与 u 是完全互斥模式,而不是互补模式。 因此,v 和 u 标志不能组合 。比如:尝试在同一正则表达式上使用这两个标志会导致错误。
3. RegExp的u和v标识
3.1 字符串的 Unicode 属性和 v 标志使用示例
Unicode 标准为每个符号分配各种属性和属性值。 例如,要获取希腊字母中使用的符号集,可以在 Unicode 数据库中搜索 Script_Extensions 属性值包含希腊字母的符号。
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 定义了一个名为“Emoji”的字符属性。
const re = /^\p{Emoji}$/u;
// 匹配仅包含 1 个代码点的表情符号:
re.test("?"); // '\u26BD'
// → true ?
// 匹配由多个代码点组成的表情符号:
re.test("???");
// '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → false ?
在上面的示例中,正则表达式与 ??? 表情符号不匹配,因为它恰好由多个代码点组成,而表情符号是 Unicode 字符属性。
幸运的是,Unicode 标准还定义了字符串的其他几个属性。 此类属性扩展为一组字符串,每个字符串都包含一个或多个代码点。 在正则表达式中,字符串的属性转换为一组替代项。 为了说明这一点,想象一下适用于字符串“a”、“b”、“c”、“W”、“xy”和“xyz”的 Unicode 属性。 此属性转换为以下任一正则表达式模式(使用交替):xyz|xy|a|b|c|W 或 xyz|xy|[a-cW]。 (首先是最长的字符串,这样像“xy”这样的前缀就不会隐藏像“xyz”这样的较长字符串。)与现有的 Unicode 属性转义不同,此模式可以匹配多字符字符串。 以下是使用字符串属性的示例:
const re = /^\p{RGI_Emoji}$/v;
// 匹配仅包含 1 个代码点的表情符号:
re.test("?"); // '\u26BD'
// → true ?
// 匹配由多个代码点组成的表情符号:
re.test("???"); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ?
此代码片段引用字符串 RGI_Emoji 的属性,Unicode 将其定义为“建议用于一般交换的所有有效表情符号(字符和序列)的子集”。 有了这个,开发者现在可以匹配表情符号,无论它们在 JavaScript Engine 下包含多少个代码点。
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 标准定义了字符串的其他属性,受支持的属性列表将来可能会增加。 尽管字符串当前的所有属性恰好都与表情符号相关,但字符串的未来属性可能会服务于不同用例。
3.2 集合表示法+字符串文字语法
使用 \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
union求并集
以前,通过使用像 [\p{Letter}\p{Number}] 这样的字符类,单字符字符串已经可以匹配 A 或 B 中的字符串。 使用 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;
// 标志序列,由 2 个代码点组成(比利时国旗):
reFlag.test(""); // → true
// 标签序列,由 7 个代码点组成(英格兰国旗):
reFlag.test(""); // → true
// 标志序列,由 2 个代码点组成(瑞士国旗):
reFlag.test(""); // → true
// 标签序列,由 7 个代码点组成(威尔士国旗):
reFlag.test(""); // → true
3.3 改进了不区分大小写的匹配
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 标志是否设置。
3.4 正则表达式 v 标志位支持
V8 v11.0 (Chrome 110) 通过 --harmony-regexp-unicode-sets 标志为这一新功能提供实验性支持。 V8 v12.0 (Chrome 112) 默认启用新功能。 Babel 还支持转译 v 标志——在 Babel REPL 中尝试本文中的示例! 下面的支持表链接到您可以订阅更新的跟踪问题。
Chrome:从 112 版本开始支持 Firefox:从 116 版本开始支持 Safari:从版本 17 开始支持 Node.js:从 version20 开始支持
4.本文总结
本文主要和大家介绍 JavaScript 基础中的 RegExp,重点在于介绍 RegExp 最新支持的标志位 v。相信通过本文的阅读,大家对 RegExp 会有一个初步的了解。
因为篇幅有限,关于 RegExp v 的更多用法和特性文章并没有过多展开,如果有兴趣,可以在我的主页继续阅读,同时文末的参考资料提供了大量优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏,您的支持是我不断创作的动力。
参考资料
https://v8.dev/features/regexp-v-flag
https://mathiasbynens.be/notes/es6-unicode-regex
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky#description
https://mathiasbynens.be/notes/es-regexp-proposals#dotAll
https://blog.csdn.net/LinBilin_/article/details/103230847
https://www.gohired.in/2017/12/22/regular-expression-matching/
https://ictis.cz/regexp-priklad-pouziti-regularni-vyraz-ohraniceni/