译者:smartsrh
原文链接
在本文中,我们将看看 ES6 及未来的正则表达式。有一些在 ES6 中引入的新正则表达式标志:粘贴匹配标志 /y
和 Unicode 标志 /u
。 然后我们将讨论 TC39 的 ECMAScript规范开发过程 上的五个提案。
粘贴匹配标志 /y
在 ES6 中引入的粘性匹配 y
标志与全局标志 g
类似。像全局正则表达式一样,粘性通常用于匹配多次,直到输入字符串的结尾。粘性正则表达式将 lastIndex
移动到上一个匹配之后的位置,就像全局正则表达式一样。唯一的区别是,粘性正则表达式必须从前一个匹配结束的位置开始匹配,不同于全局正则表达式在任何给定位置不匹配时会移动到输入字符串的其余部分继续匹配。
以下示例说明了两者之间的区别。给出一个输入字符串如 'haha haha haha'
和正则表达式 /ha/
,全局标志将匹配每一个 'ha'
,而粘标志只匹配前两个,因为第三次出现不在起始索引 4
,而是索引 5
。
function matcher (regex, input) {
return () => {
const match = regex.exec(input)
const lastIndex = regex.lastIndex
return { lastIndex, match }
}
}
const input = 'haha haha haha'
const nextGlobal = matcher(/ha/g, input)
console.log(nextGlobal()) // <- { lastIndex: 2, match: ['ha'] }
console.log(nextGlobal()) // <- { lastIndex: 4, match: ['ha'] }
console.log(nextGlobal()) // <- { lastIndex: 7, match: ['ha'] }
const nextSticky = matcher(/ha/y, input)
console.log(nextSticky()) // <- { lastIndex: 2, match: ['ha'] }
console.log(nextSticky()) // <- { lastIndex: 4, match: ['ha'] }
console.log(nextSticky()) // <- { lastIndex: 0, match: null }
如果我们用下一个代码强力移动 lastIndex
,我们可以验证粘性匹配器是可以正常工作的。
const rsticky = /ha/y
const nextSticky = matcher(rsticky, input)
console.log(nextSticky()) // <- { lastIndex: 2, match: ['ha'] }
console.log(nextSticky()) // <- { lastIndex: 4, match: ['ha'] }
rsticky.lastIndex = 5 console .log(nextSticky()) // <- { lastIndex: 7, match: ['ha'] }
将粘性匹配添加到 JavaScript 中是为了改进编译器中的性能,因为词法分析器严重依赖正则表达式。
Unicode 标志 /u
ES6 还引入了一个 u
标志。 u
代表 Unicode,但是这个标志也可以被认为是更严格的正则表达式。
没有 u
标志,以下代码段是一个包含不必要转义的 'a'
字符文字的正则表达式。
/\a/.test('ab') // <- true
在带有 u
标志的正则表达式中使用非保留字符像 a
的转义形式会导致错误,如下面的代码位所示。
/\a/u.test('ab') // <- SyntaxError: Invalid regular expression: /\a/: Invalid escape`
ES6 中增加了像 '\u{1f40e}'
等字符串,以下示例尝试通过用 \u{1f40e}
将马表情符号嵌入正则表达式中,但正则表达式无法与马表情符号匹配。没有 u
标志,\u{…}
模式被解释为具有不必要的转义的 u
字符和其后的其余部分。
/\u{1f40e}/.test('?') // <- false
/\u{1f40e}/.test('u{1f40e}') // <- true`
u
标志支持了正则表达式中 Unicode 代码转义,如 \u{1f40e}
马表情符号。
/\u{1f40e}/u.test('?') // <- true
没有 u
标志,.
会匹配任何 BMP 符号,除了行终止符和 astral 字符。以下示例是音乐中的高音谱号 ?,这是一种在普通正则表达式中不会被 .
匹配的 astral 符号。
const rdot = /^.$/
rdot.test('a') // <- true
rdot.test('\n') // <- false
rdot.test('\u{1d11e}') // <- false
当使用 u
标志时,不属于 BMP 的 Unicode 符号也会被匹配。下一个片段显示了 ? 符号在设置标志后如何被匹配。
const rdot = /^.$/u
rdot.test('a') // <- true
rdot.test('\n') // <- false
rdot.test('\u{1d11e}') // <- true
当 u
标志被设置时,可以在数字和字符类中找到 Unicode 字符,这两者都将每个 Unicode 代码视为单个符号,而不是仅在第一个字符单元上进行匹配。i
标志匹配对大小写不敏感可以 u
标志设置时匹配 Unicode 的字母,用于对输入字符串和正则表达式中的代码进行归一化。
有关正则表达式中 u
标志的更多详细信息,请参阅 Mathias Bynens 的文章 。
命名捕获组
到目前为止,JavaScript 正则表达式可以对编号捕获组和非捕获组中的匹配进行分组。 在下一个片段中,我们使用分组来从包含由 '='
分隔的键值对的输入字符串中提取键和值。
function parseKeyValuePair(input) {
const rattribute = /([a-z]+)=([a-z]+)/
const [, key, value] = rattribute.exec(input)
return { key, value }
}
parseKeyValuePair( 'strong=true' )
// <- { key: 'strong', value: 'true' }
还有被丢弃的非捕获组,不存在于最终结果中,但仍然可用于匹配。以下示例支持使用 ' is '
和 '='
分隔的键值对的匹配。
function parseKeyValuePair(input) {
const rattribute = /([a-z]+)(?:=|\sis\s)([a-z]+)/
const [, key, value] = rattribute.exec(input)
return { key, value }
}
parseKeyValuePair( 'strong is true' ) // <- { key: 'strong', value: 'true' }
parseKeyValuePair( 'flexible=too' ) // <- { key: 'flexible', value: 'too' }
尽管上一个示例中的数组解构隐藏了我们的代码对神奇的数组索引的依赖,但事实仍然是匹配是被放置在有序数组中的。命名捕获组提案 (在撰写本文时已处于第 3 阶段) 添加了类似 (?<groupName>)
的语法,我们可以在其中命名捕获组,然后其将返回到返回的匹配对象的 groups
属性中。当调用 RegExp#exec
或 String#match
时,groups
属性可以从结果对象中进行解析。
function parseKeyValuePair (input) {
const rattribute = /(?<key>[a-z]+)(?:=|\sis\s)(?<value>[a-z]+)/u
const { groups } = rattribute.exec(input)
return groups
}
parseKeyValuePair( 'strong=true' ) // <- { key: 'strong', value: 'true' }
parseKeyValuePair( 'flexible=too' ) // <- { key: 'flexible', value: 'too' }
JavaScript 正则表达式支持反向引用,捕获的组可以重用于查找重复项。以下代码段使用第一个捕获组的反向引用来识别用户名与 'user:password'
输入中的密码相同的情况。
function hasSameUserAndPassword(input) {
const rduplicate = /([^:]+):\1/
return rduplicate.exec(input) !== null
}
hasSameUserAndPassword('root:root') // <- true
hasSameUserAndPassword('root:pF6GGlyPhoy1!9i') // <- false
命名的捕获组提案增加了对反向引用命名的支持。
function hasSameUserAndPassword(input) {
const rduplicate = /(?<user>[^:]+):\k<user>/u
return rduplicate.exec(input) !== null
}
hasSameUserAndPassword('root:root') // <- true
hasSameUserAndPassword('root:pF6GGlyPhoy1!9i') // <- false
\k<groupName>
引用可以与编号引用一起使用,已经使用命名引用时就尽量避免使用后者。
最后,可以在传递给 String#replace
的替换中引用命名组。在下一个代码片段中,我们使用 String#replace
和命名组来把美国的日期字符串更改成匈牙利格式。
function americanDateToHungarianFormat(input) {
const ramerican = /(?<month>\d{2})\/(?<day>\d{2})\/(?<year>\d{4})/u
const hungarian = input.replace(ramerican, '$<year>-$<month>-$<day>')
return hungarian
}
americanDateToHungarianFormat('06/09/1988') // <- '1988-09-06'
如果 String#replace
的第二个参数是一个函数,则可以通过参数列表末尾的 groups
来访问命名组。该功能的要求参数现在是 (match, ...captures, groups)
。在以下示例中,请注意我们如何使用类似于上一个示例中替换字符串的模板文字。事实上,替换字符串遵循 $<groupName>
语法而不是 ${groupName}
语法,这意味着如果我们使用模板文字,我们可以在替换字符串中命名组,而无需使用转义代码。
function americanDateToHungarianFormat(input) {
const ramerican = /(?<month>\d{2})\/(?<day>\d{2})\/(?<year>\d{4})/u
const hungarian = input.replace(ramerican, (match, capture1, capture2, capture3, groups) => {
const { month, day, year } = groups
return `${ year }-${ month }-${ day }`
})
return hungarian
}
americanDateToHungarianFormat( '06/09/1988' ) // <- '1988-09-06'
Unicode 属性转义
Unicode属性转义提案 _(目前在第 3 阶段)_是一种新的转义序列,可在有 u
标志的正则表达式中使用。该提案以\p{LoneUnicodePropertyNameOrValue}
的形式为二进制 Unicode 属性和\p{UnicodePropertyName=UnicodePropertyValue}
为非二进制 Unicode 属性添加了转义。另外,\P
是 \p
转义序列的否定版本。
Unicode 标准为每个符号定义了属性。拥有这些属性,可以对 Unicode 字符进行高级查询。例如,希腊字母表中的符号具有设置为 Greek
的 Script
属性。我们可以使用新的转义来匹配任何希腊语 Unicode 符号。
function isGreekSymbol(input) {
const rgreek = /^\p{Script=Greek}$/u
return rgreek.test(input)
}
isGreekSymbol('π') // <- true
或者,使用 \P
,我们可以匹配非希腊语 Unicode 符号。
function isNonGreekSymbol(input) {
const rgreek = /^\P{Script=Greek}$/u
return rgreek.test(input)
}
isNonGreekSymbol('π') // <- false
当我们需要匹配每个 Unicode 十进制数字符号,而不只是像 \d
这样的 [0-9]
,我们可以使用 \p{Decimal_Number}
如下所示。
function isDecimalNumber(input) {
const rdigits = /^\p{Decimal_Number}+$/u
return rdigits.test(input)
}
isDecimalNumber( '????????????????' ) // <- true
下面的链接是支持的 Unicode 属性和值的完整列表。
后行(Lookbehind)断言
JavaScript 很早就有阳性的先行断言(lookahead assertions)。该功能允许我们匹配一个表达式,并且它的后面是另一个表达式。这些断言表示为 (?=…)
。无论先行断言是否匹配,该匹配的结果将被丢弃,并且不会输入输入字符串的字符。
以下示例使用一个阳性的先行断言测试输入字符串是否以 .js
结尾,在是的情况下,它将返回没有 .js
部分的文件名。
function getJavaScriptFilename(input) {
const rfile = /^(?<filename>[a-z]+)(?=\.js)\.[a-z]+$/u
const match = rfile.exec(input)
if (match === null ) {
return null
}
return match.groups.filename
}
getJavaScriptFilename( 'index.js' ) // <- 'index'
getJavaScriptFilename( 'index.php' ) // <- null
还有阴性的先行断言,其表达为 (?!…)
而不是阳性先行断言的 (?=…)
。在这种情况下,仅当先行断言不匹配时,断言才会成功。下面的代码使用了阴性的先行断言,我们可以观察结果如何不同:现在除 '.js'
之外的任何表达式都会导致断言成功。
function getNonJavaScriptFilename(input) {
const rfile = /^(?<filename>[az]+)(?!\.js)\.[az]+$/u
const match = rfile.exec(input)
if (match === null ) {
return null
}
return match.groups.filename
}
getNonJavaScriptFilename('index.js') // <- null
getNonJavaScriptFilename('index.php') // <- 'index'
后行断言提案 (第 3 阶段) 引入了阳性和阴性的后行断言,分别用 (?<=…)
和 (?<!…)
表示。这些断言可用于确保我们想要匹配的片段是不是紧跟在另一个给定片段之后。以下代码段使用阳性的后行断言来匹配美元金额的数字,但不匹配欧元。
function getDollarAmount(input) {
const rdollars = /^(?<=\$)(?<amount>\d+(?:\.\d+)?)$/u
const match = rdollars.exec(input)
if (match === null ) {
return null
}
return match.groups.amount
}
getDollarAmount('$12.34') // <- '12.34'
getDollarAmount('€12.34') // <- null
另一方面,可以使用阴性的后行来匹配非美元符号的数字。
function getNonDollarAmount (input) {
const rnumbers = /^(?<!\$)(?<amount>\d+(?:\.\d+)?)$/u
const match = rnumbers.exec(input)
if (match === null ) {
return null
}
return match.groups.amount
}
getNonDollarAmount('$12.34') // <- null
getNonDollarAmount('€12.34') // <- '12.34'
一个新的 /s
_( dotAll
)_标志
使用 .
时我们通常期望匹配每一个字符。然而,在JavaScript中,一个 .
表达式不匹配 astral 符号_(可以通过添加 u
标志来修正)_,也不匹配行终止符。
const rcharacter = /^.$/
rcharacter.test('a') // <- true
rcharacter.test('\t') // <- true
rcharacter.test('\n') // <- false
这有时迫使开发人员编写其他类型的表达式来合成一个匹配任何字符的正则表达式。下一代码中的表达式匹配任何一个空格字符或非空白字符的字符,从而提供我们期望的 .
匹配行为
const rcharacter = /^[\s\S]$/
rcharacter.test('a') // <- true
rcharacter.test('\t') // <- true
rcharacter.test('\n') // <- true
dotAll
提案 _(第 3 阶段)_添加了改变 .
行为的s
标志,可以在 JavaScript 正则表达式中匹配任何单个字符。
const rcharacter = /^.$/s
rcharacter.test('a') // <- true
rcharacter.test('\t') // <- true
rcharacter.test('\n') // <- true
String#matchAll
通常,当我们有一个具有全局或粘性标志的正则表达式时,我们想迭代捕获组集合中的每个匹配。目前,产生匹配列表可能有点麻烦:我们需要使用 String#match
或 RegExp#exec
在循环中收集捕获的组,直到正则表达式与最后一个 lastIndex
开始的输入不匹配为止。在下面的代码段中,parseAttributes
生成器函数只针对给定的正则表达式。
function *parseAttributes(input) {
const rattributes = /(\w+)=""([^""]+)""\s/ig
while (true) {
const match = rattributes.exec(input)
if (match === null ) {
break
}
const [ , key, value] = match
yield [key, value]
}
}
const html = '<input type=""email"" placeholder=""hello@mjavascript.com"" />'
console.log(...parseAttributes(html))
// <- ['type', 'email'] ['placeholder', 'hello@mjavascript.com']
这种方法的一个问题是它是针对我们的正则表达式及其捕获组而量身定制的。我们可以通过创建一个 matchAll
生成器来解决这个问题,该生成器只关心循环匹配和收集捕获组的集合,如下面的代码片段所示。
function *matchAll(regex, input) {
while (true) {
const match = regex.exec(input)
if (match === null ) {
break
}
const [ , ...captures] = match
yield captures
}
}
function *parseAttributes(input) {
const rattributes = /(\w+)=""([^""]+)""\s/ig
yield *matchAll(rattributes, input)
}
const html = '<input type=""email"" placeholder=""hello@mjavascript.com"" />'
console.log(...parseAttributes(html))
// <- ['type', 'email'] ['placeholder', 'hello@mjavascript.com']
一个更大的困惑是,在每次调用 RegExp#exec
时,这个 rattributes
会改变其 lastIndex
属性,这使得它可以记录最后一次匹配的位置。当没有匹配的时候,lastIndex
会重新设置为 0
。当我们不一次性迭代一个输入的所有可能匹配时,会出现一个问题 —— 这会将 lastIndex
重置为 0
—— 然后我们在第二个输入上使用这个正则表达式,会获得意想不到的结果。
虽然看起来我们的 matchAll
实现不会成为这个问题的受害者,但是由于可以用生成器手动循环遍历所有匹配项,这意味着如果我们重复使用相同的正则表达式,我们就会遇到麻烦,如下代码所示。请注意,第二个匹配器本应该匹配怎样的 ['type', 'text']
,而实际上却是从比 0
更远的索引开始匹配,甚至将 'placeholder'
键错匹配为 'laceholder'
。
const rattributes = /(\w+)=""([^""]+)""\s/ig
const email = '<input type=""email"" placeholder=""hello@mjavascript.com"" />'
const emailMatcher = matchAll(rattributes, email)
const address = '<input type=""text"" placeholder=""Enter your business address"" />'
const addressMatcher = matchAll(rattributes, address)
console.log(emailMatcher.next().value) // <- ['type', 'email']
console.log(addressMatcher.next().value) // <- ['laceholder', 'Enter your business address']
一个解决方案是改变 matchAll
,使得当我们 yield 给 *parseAttributes
时,lastIndex
总是 0
,同时在内部跟踪 lastIndex
,以便我们可以在序列中从上次暂停的步骤开始执行。
以下代码显示,确实,这解决了我们提到的问题。 由于这个原因,经常会避免使用可重用的全局正则表达式:这样我们就不用担心每次使用后重新lastIndex
。
function *matchAll(regex, input) {
let lastIndex = 0
while (true) {
regex.lastIndex = lastIndex
const match = regex.exec(input)
if (match === null) {
break
}
lastIndex = regex.lastIndex
regex.lastIndex = 0
const [ , ...captures] = match
yield captures
}
}
const rattributes = /(\w+)=""([^""]+)""\s/ig
const email = '<input type=""email"" placeholder=""hello@mjavascript.com"" />'
const emailMatcher = matchAll(rattributes, email)
const address = '<input type=""text"" placeholder=""Enter your business address"" />'
const addressMatcher = matchAll(rattributes, address)
console.log(emailMatcher.next().value) // <- ['type', 'email']
console.log(addressMatcher.next().value) // <- ['type', 'text']
console.log(emailMatcher.next().value) // <- ['placeholder', 'hello@mjavascript.com']
console.log(addressMatcher.next().value) // <- ['placeholder', 'Enter your business address']
String#matchAll
提案 _(撰写本文时在第一阶段)_在字符串原型上定义了一个新方法,它将与我们的 matchAll
类似的实现方式运行,除了返回的 iterable 是一个match
对象的序列,而不仅仅是上面的例子中的 captures
。请注意,String#matchAll
序列包含整个 match
对象,而不仅仅是编号的捕获。这意味着我们可以通过 match.groups
为序列中的每个 match
访问命名捕获。
const rattributes = /(?<key>\w+)=""(?<value>[^""]+)""\s/igu
const email = '<input type=""email"" placeholder=""hello@mjavascript.com"" />'
for ( const { groups: { key, value } } of email.matchAll(rattributes)) {
console .log(`${ key }: ${ value }`)
}
// <- type: email
// <- placeholder: hello@mjavascript.com`
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!