正则表达式在 ES2018 中的新写法
翻译:疯狂的技术宅
原文:https://www.smashingmagazine....
本文首发微信公众号:jingchengyideng
欢迎关注,每天都给你推送新鲜的前端技术文章
摘要:如果你曾用 JavaScript 做过复杂的文本处理和操作,那么你将会对 ES2018 中引入的新功能爱不释手。 在本文中,我们将详细介绍第 9 版标准如何提高 JavaScript 的文本处理能力。
有一个很好的理由能够解释为什么大多数编程语言都支持正则表达式:它们是用于处理文本的极其强大的工具。 通常一行正则表达式代码就能完成需要几十行代码才能搞定的文本处理任务。 虽然大多数语言中的内置函数足以对字符串进行一般的搜索和替换操作,但更加复杂的操作(例如验证文本输入)通常需要使用正则表达式。
自从 1999 年推出 ECMAScript 标准第 3 版以来,正则表达式已成为 JavaScript 语言的一部分。ECMAScript 2018(简称ES2018)是该标准的第 9 版,通过引入四个新功能进一步提高了JavaScript的文本处理能力:
下面详细介绍这些新功能。
后行断言
能够根据之后或之前的内容匹配一系列字符,使你可以丢弃可能不需要的匹配。 当你需要处理大字符串并且意外匹配的可能性很高时,这个功能非常有用。 幸运的是,大多数正则表达式都为此提供了 lookbehind 和 lookahead 断言。
在 ES2018 之前,JavaScript 中只提供了先行断言。 lookahead 允许你在一个断言模式后紧跟另一个模式。
先行断言有两种版本:正向和负向。 正向先行断言的语法是 (?=...)
。 例如,正则表达式 /Item(?= 10)/
仅在后面跟随有一个空格和数字 10 的时候才与 Item
匹配:
const re = /Item(?= 10)/; console.log(re.exec('Item')); // → null console.log(re.exec('Item5')); // → null console.log(re.exec('Item 5')); // → null console.log(re.exec('Item 10')); // → ["Item", index: 0, input: "Item 10", groups: undefined]
此代码使用 exec()
方法在字符串中搜索匹配项。 如果找到匹配项, exec()
将返回一个数组,其中第一个元素是匹配的字符串。 数组的 index
属性保存匹配字符串的索引, input
属性保存搜索执行的整个字符串。 最后,如果在正则表达式中使用了命名捕获组,则将它们放在 groups
属性中。 在代码中, groups
的值为 undefined
,因为没有被命名的捕获组。
负向先行的构造是 (?!...)
。 负向先行断言的模式后面没有特定的模式。 例如, /Red(?!head)/
仅在其后不跟随 head
时匹配 Red
:
const re = /Red(?!head)/; console.log(re.exec('Redhead')); // → null console.log(re.exec('Redberry')); // → ["Red", index: 0, input: "Redberry", groups: undefined] console.log(re.exec('Redjay')); // → ["Red", index: 0, input: "Redjay", groups: undefined] console.log(re.exec('Red')); // → ["Red", index: 0, input: "Red", groups: undefined]
ES2018 为 JavaScript 补充了后行断言。 用 (?<=...)
表示,后行断言允许你在一个模式前面存在另一个模式时进行匹配。
假设你需要以欧元检索产品的价格但是不捕获欧元符号。 通过后行断言,会使这项任务变得更加简单:
const re = /(?<=€)\d+(\.\d*)?/; console.log(re.exec('199')); // → null console.log(re.exec('$199')); // → null console.log(re.exec('€199')); // → ["199", undefined, index: 1, input: "€199", groups: undefined]
注意:先行(Lookahead)和后行(lookbehind)断言通常被称为“环视”(lookarounds)。
后行断言的反向版本由 (?<!...)
表示,使你能够匹配不在lookbehind中指定的模式之前的模式。 例如,正则表达式 /(?<!\d{3}) meters/
会在 三个数字不在它之前 匹配单词“meters”如果:
const re = /(?<!\d{3}) meters/; console.log(re.exec('10 meters')); // → [" meters", index: 2, input: "10 meters", groups: undefined] console.log(re.exec('100 meters')); // → null
与前行断言一样,你可以连续使用多个后行断言(负向或正向)来创建更复杂的模式。下面是一个例子:
const re = /(?<=\d{2})(?<!35) meters/; console.log(re.exec('35 meters')); // → null console.log(re.exec('meters')); // → null console.log(re.exec('4 meters')); // → null console.log(re.exec('14 meters')); // → ["meters", index: 2, input: "14 meters", groups: undefined]
此正则表达式仅匹配包含“meters”的字符串,如果它前面紧跟 35 之外的任何两个数字。正向后行确保模式前面有两个数字,同时负向后行能够确保该数字不是 35。
命名捕获组
你可以通过将字符封装在括号中的方式对正则表达式的一部分进行分组。 这可以允许你将规则限制为模式的一部分或在整个组中应用量词。 此外你可以通过括号来提取匹配值并进行进一步处理。
下列代码给出了如何在字符串中查找带有 .jpg 并提取文件名的示例:
const re = /(\w+)\.jpg/; const str = 'File name: cat.jpg'; const match = re.exec(str); const fileName = match[1]; // The second element in the resulting array holds the portion of the string that parentheses matched console.log(match); // → ["cat.jpg", "cat", index: 11, input: "File name: cat.jpg", groups: undefined] console.log(fileName); // → cat
在更复杂的模式中,使用数字引用组只会使本身就已经很神秘的正则表达式的语法更加混乱。 例如,假设你要匹配日期。 由于在某些国家和地区会交换日期和月份的位置,因此会弄不清楚究竟哪个组指的是月份,哪个组指的是日期:
const re = /(\d{4})-(\d{2})-(\d{2})/; const match = re.exec('2020-03-04'); console.log(match[0]); // → 2020-03-04 console.log(match[1]); // → 2020 console.log(match[2]); // → 03 console.log(match[3]); // → 04
ES2018针对此问题的解决方案名为捕获组,它使用更具表现力的 (?<name>...)
形式的语法:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const match = re.exec('2020-03-04'); console.log(match.groups); // → {year: "2020", month: "03", day: "04"} console.log(match.groups.year); // → 2020 console.log(match.groups.month); // → 03 console.log(match.groups.day); // → 04
因为生成的对象可能会包含与命名组同名的属性,所以所有命名组都在名为 groups
的单独对象下定义。
许多新的和传统的编程语言中都存在类似的结构。 例如Python对命名组使用 (?P<name>)
语法。 Perl支持与 JavaScript 相同语法的命名组( JavaScript 已经模仿了 Perl 的正则表达式语法)。 Java也使用与Perl相同的语法。
除了能够通过 groups
对象访问命名组之外,你还可以用编号引用访问组—— 类似于常规捕获组:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const match = re.exec('2020-03-04'); console.log(match[0]); // → 2020-03-04 console.log(match[1]); // → 2020 console.log(match[2]); // → 03 console.log(match[3]); // → 04
新语法也适用于解构赋值:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const [match, year, month, day] = re.exec('2020-03-04'); console.log(match); // → 2020-03-04 console.log(year); // → 2020 console.log(month); // → 03 console.log(day); // → 04
即使正则表达式中不存在命名组,也始终创建 groups
对象:
const re = /\d+/; const match = re.exec('123'); console.log('groups' in match); // → true
如果可选的命名组不参与匹配,则 groups
对象仍将具有命名组的属性,但该属性的值为 undefined
:
const re = /\d+(?<ordinal>st|nd|rd|th)?/; let match = re.exec('2nd'); console.log('ordinal' in match.groups); // → true console.log(match.groups.ordinal); // → nd match = re.exec('2'); console.log('ordinal' in match.groups); // → true console.log(match.groups.ordinal); // → undefined
你可以稍后在模式中引用常规捕获的组,并使用 \1
的形式进行反向引用。 例如以下代码使用在行中匹配两个字母的捕获组,然后在模式中调用它:
console.log(/(\w\w)\1/.test('abab')); // → true // if the last two letters are not the same // as the first two, the match will fail console.log(/(\w\w)\1/.test('abcd')); // → false
要在模式中稍后调用命名捕获组,可以使用 /\k<name>/
语法。 下面是一个例子:
const re = /\b(?<dup>\w+)\s+\k<dup>\b/; const match = re.exec("I'm not lazy, I'm on on energy saving mode"); console.log(match.index); // → 18 console.log(match[0]); // → on on
此正则表达式在句子中查找连续的重复单词。 如果你愿意,还可以用带编号的后引用来调用命名的捕获组:
const re = /\b(?<dup>\w+)\s+\1\b/; const match = re.exec("I'm not lazy, I'm on on energy saving mode"); console.log(match.index); // → 18 console.log(match[0]); // → on on
也可以同时使用带编号的后引用和命名后向引用:
const re = /(?<digit>\d):\1:\k<digit>/; const match = re.exec('5:5:5'); console.log(match[0]); // → 5:5:5
与编号的捕获组类似,可以将命名的捕获组插入到 replace()
方法的替换值中。 为此,你需要用到 $<name>
构造。 例如:
const str = 'War & Peace'; console.log(str.replace(/(War) & (Peace)/, '$2 & $1')); // → Peace & War console.log(str.replace(/(?<War>War) & (?<Peace>Peace)/, '$<Peace> & $<War>')); // → Peace & War
如果要使用函数执行替换,则可以引用命名组,方法与引用编号组的方式相同。 第一个捕获组的值将作为函数的第二个参数提供,第二个捕获组的值将作为第三个参数提供:
const str = 'War & Peace'; const result = str.replace(/(?<War>War) & (?<Peace>Peace)/, function(match, group1, group2, offset, string) { return group2 + ' & ' + group1; }); console.log(result); // → Peace & War
s
(dotAll
) Flag
默认情况下,正则表达式模式中的点 (.
) 元字符匹配除换行符 (\n
) 和回车符 (\r
)之外的所有字符:
console.log(/./.test('\n')); // → false console.log(/./.test('\r')); // → false
尽管有这个缺点,JavaScript 开发者仍然可以通过使用两个相反的速记字符类来匹配所有字符,例如[ w W],它告诉正则表达式引擎匹配一个字符(\w
)或非单词字符(\w
):
console.log(/[\w\W]/.test('\n')); // → true console.log(/[\w\W]/.test('\r')); // → true
ES2018旨在通过引入 s
(dotAll
) 标志来解决这个问题。 设置此标志后,它会更改点 (.
)元字符的行为以匹配换行符:
console.log(/./s.test('\n')); // → true console.log(/./s.test('\r')); // → true
s
标志可以在每个正则表达式的基础上使用,因此不会破坏依赖于点元字符的旧行为的现有模式。 除了 JavaScript 之外, s
标志还可用于许多其他语言,如 Perl 和 PHP。
Unicode 属性转义
ES2015中引入的新功能包括Unicode感知。 但是即使设置了 u
标志,速记字符类仍然无法匹配Unicode字符。
请考虑以下案例:
const str = '