专栏名称: 众成翻译
翻译,求知的另一种表达
目录
相关文章推荐
中核集团  ·  面向全社会发布! ·  9 小时前  
青年文摘  ·  0.8元的充电器和249元的,到底有啥区别?🧐 ·  17 小时前  
中核集团  ·  卓越绩效大家谈⑥ ·  2 天前  
51好读  ›  专栏  ›  众成翻译

后 ES6 时代的正则匹配

众成翻译  · 掘金  ·  · 2021-02-04 19:47

正文

阅读 61

后 ES6 时代的正则匹配

译者: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 正则表达式中匹配任何单个字符。







请到「今天看啥」查看全文


推荐文章
中核集团  ·  面向全社会发布!
9 小时前
中核集团  ·  卓越绩效大家谈⑥
2 天前
冰点周刊  ·  别了,迷路的抹香鲸
7 年前
汉坤律师事务所  ·  亚投行合规局局长Hamid Sharif先生莅临汉坤演讲
7 年前