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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,219评论 0 4
  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 2,892评论 0 16
  • 概述 正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模...
    许先生__阅读 271评论 0 1
  •   引用类型的值(对象)是引用类型的一个实例。   在 ECMAscript 中,引用类型是一种数据结构,用于将数...
    霜天晓阅读 1,044评论 0 1
  • 残疾人,是特殊的人群。他们总被人用异样的眼神小心翼翼的瞄着,伴随着故意压低的讨论声,眼里有些许可怜,还有些许害怕,...
    问怀君阅读 402评论 0 5