ES 提案: String.prototype.matchAll

原文:ES proposal: String.prototype.matchAll

Jordan Harband的提案“String.prototype.matchAll”目前处于第3阶段。这篇博客将会解释它如何工作的。

在我们看这个提案前,回顾下现状。

1.用一个正则表达式来得到所有匹配项。

目前,您可以通过几种方式获取给定正则表达式的所有匹配项。

1. RegExp.prototype.exec() 与 /g

如果正则表达式有/g标志,那么多次调用.exec()就会得到所有匹配的结果。如果没有匹配的结果,.exec()就会返回null。在这之前会返回每个匹配的匹配对象。这个对象包含捕获的子字符串和更多信息。

举个例子:得到所有双引号之间的字符串

function collectGroup1(regExp, str) {
  const matches = [];
  while (true) {
     const match = regExp.exec(str);
     if (match === null) break;
     // 把match中捕获的字符串,加到matches中。
     matches.push(match[1]);
   }
     return matches;
} 
// /"([^"]*)"/ug 匹配所有双引号与其之间的内容,并捕获所有引号间的信息。
collectGroup1(/"([^"]*)"/ug,`"foo" and "bar" and "baz"`);
 // [ 'foo', 'bar', 'baz' ]

如果正则表达式没有/g标志,.exec()总是返回第一次匹配的结果。

> let re = /[abc]/;
> re.exec('abc')
[ 'a', index: 0, input: 'abc' ]
> re.exec('abc')
[ 'a', index: 0, input: 'abc' ]

这样的话对函数collectGroup1就是一个坏消息,因为如果没有/g标志,函数无法结束运行,此时match就一直是第一次匹配的结果,循环永远无法break。

为什么会这样?

因为正则表达式有一个lastIndex(初始值为0)属性,每次.exec()前,都会根据lastIndex属性的值来决定开始匹配的位置。

如果正则表达式没有/g标志,那么运行一次.exec()时,不会改变lastIndex的值,导致下一次运行exec()时,匹配仍旧是从字符串0的位置开始。

当正则表达式加了/g标志后,运行一次exec(),正则表达式的lastIndex就会改变,下次运行exec()就会从前一次的结果之后开始匹配。

2.String.prototype.match() 与 /g

你可以使用.match()方法和一个带有/g标志的正则表达式,你就可以得到一个数组,包含所有匹配的结果(换句话说,所有捕获组都将被忽略)。

> "abab".match(/a/ug)
[ 'a', 'a' ]

如果/g标志没有被设置,那么.match()与RegExp.prototype.exec()返回的结果一样。

> "abab".match(/a/u)
[ 'a', index: 0, input: 'abab' ]

3.String.prototype.replace() 与 /g

你可以用一个小技巧来收集所有的捕获组——通过.replace()。replace函数接收一个能够返回要替换的值的函数,这个函数能够接收所有的捕获信息。但是,我们不用这个函数去计算替换的值,而是在函数里用一个数组去收集感兴趣的数据。

function collectGroup1(regExp, str) {
    const matches = [];
    function replacementFunc(all, first) {
        matches.push(first);
    }
    str.replace(regExp, replacementFunc);
    return matches;
}
collectGroup1(/"([^"]*)"/ug,`"foo" and "bar" and "baz"`);
 // [ 'foo', 'bar', 'baz' ]

对于没有/g标志的正则表达式,.replace()仅访问第一个匹配项。

4.RegExp.prototype.test()

.test()只要正则表达式匹配成功就会返回true。

const regExp = /a/ug;
const str = 'aa';
regExp.test(str); // true
regExp.test(str); // true
regExp.test(str); // false

5.String.prototype.split()

你可以拆分一个字符串并用一个正则表达式去指定分隔符。如果正则表达式包含至少一个捕获组,那么.split()将会返回一个数组,其中结果会跟第一个捕获组互相交替。

const regExp = /<(-+)>/ug;
const str = 'a<--->b<->c';
str.split(regExp);
// [ 'a', '---', 'b', '-', 'c' ]

const regExp = /<(?:-+)>/ug;
const str = 'a<--->b<->c';
str.split(regExp);
//[ 'a', 'b', 'c' ]

2.目前这些方法存在的问题

目前这些方法都有以下几个缺点:

1.它们是冗长且不直观的。

2.如果标志/g被设置了,它们才会工作。有时候,我们会从其他地方接收正则表达式,比如通过一个参数。如果我们想要去确定所有匹配的项都能找到,那么不得不检查/g标志有没有被设置。

3.为了跟踪进程,所有的方法(除了match)改变了正则表达式的属性,.lastIndex记录了上一次匹配的结束为止。这使得在多个为止使用相同的正则表达式会存在风险(因为正则表达式的lastIndex属性改变了,但是你还在别的地方使用这个正则表达式,那么结果可能会和你想要的不一样)。这太可惜了,当你需多次调用.exec()的时候,你不能在一个函数内联一个正则表达式。(因为每次调用,正则表达式都会之重置)。

4.由于属性.lastIndex决定在了在哪继续调用。当我们开始继续收集匹配项的时候,就必须把她始终为0。但是,至少.exec()和其他一些方法会在最后一次匹配后将.lastIndex重置为0。如果它不是零,就会发生这种情况:

const regExp = /a/ug;
regExp.lastIndex = 2;
regExp.exec('aabb'); // null

3.提案:tring.prototype.matchAll

这就是你调用.matchAll()的方式:

const matchIterator = str.matchAll(regExp);

给定一个字符串和一个正则表达式,.matchAll()为所有匹配的匹配对象返回一个迭代器。

你也可以使用一个扩展运算符…把迭代器转换为数组。

> [...'-a-a-a'.matchAll(/-(a)/ug)]
[ [ '-a', 'a' ], [ '-a', 'a' ], [ '-a', 'a' ] ]

现在是否设置/g,都不会有问题了。

> [...'-a-a-a'.matchAll(/-(a)/u)]
[ [ '-a', 'a' ], [ '-a', 'a' ], [ '-a', 'a' ] ]

使用.matchAll(),函数collectGroup1() 变得更短更容易理解了。

function collectGroup1(regExp, str) {
  let results = [];
  for (const match of str.matchAll(regExp)) {
       results.push(match[1]);
    }
    return results;
}

我们可以使用扩展运算符和.map()来使这个函数更简洁。

function collectGroup1(regExp, str) {
    let arr = [...str.matchAll(regExp)];
    return arr.map(x => x[1]);
}

另一个选择是使用Array.from(),它会同时转换数组和映射。因此,你不需要再定义中间值arr。

function collectGroup1(regExp, str) {
  return Array.from(str.matchAll(regExp), x => x[1]);
}

3.1 .matchAll() returns an iterator, not a restartable iterable

.matchAll()返回一个跌倒器,但不是一个真的可重新利用的迭代器。一旦结果耗尽,你不得不再次调用方法,获取一个新的迭代器。

相反,.match()加上 /g 返回一个迭代器即数组,只要你想,你就可以迭代它。

4.Implementing .matchAll()

你如何实现matchAll:

function ensureFlag(flags, flag) {
    return flags.includes(flag) ? flags : flags + flag;
}
function* matchAll(str, regex) {
    const localCopy = new RegExp(
    regex, ensureFlag(regex.flags, 'g'));
    let match;
    while (match = localCopy.exec(str)) {
        yield match;
    }
}

制作一个本地副本,确保了一下几件事:

  • /g被设置了
  • regex.index 不会改变
  • regex.index 是0

使用matchAll():

const str = ‘”fee” “fi” “fo” “fum”‘;
const regex = /”([^”]*)”/;

for (const match of matchAll(str, regex)) {
    console.log(match[1]);
}
// Output:
// fee
// fi
// fo
// fum

5.常见问题

5.1 为什么不是RegExp.prototype.execAll()?

一方面,.matchAll()确实跟批量调用.exec()的工作很像,因此名称.execAll()会有意义。

另一方面,exec()改变了正则表达式,而match()没有。这就解释了,为什么名字matchAll()会被选择。

6.进一步阅读

    原文作者:打铁大师
    原文地址: https://www.jianshu.com/p/7dbf4a1e6805
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞