ES 提案: String.prototype.matchAll

作者: 打铁大师 | 来源:发表于2018-02-14 22:43 被阅读425次

    原文: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.进一步阅读

    相关文章

      网友评论

        本文标题:ES 提案: String.prototype.matchAll

        本文链接:https://www.haomeiwen.com/subject/onxytftx.html