本篇介紹 ES2020 (ES11) 提供的 String.prototype.matchAll()
。
若將一個字串使用的 RegExp (regular expression,正規表達式,正規表示式) 設定了 sticky
或 global
flag,則可能會有多個 capture groups,常見的情境會想迭代所有 match 到的結果,可能會有幾種作法:
String.prototype.match()
RegExp.prototype.exec()
String.prototype.replace()
分別來介紹過去的這些作法有哪些缺點。
String.prototype.match()
若在 String.prototype.match()
使用的 RegExp 沒有設定 global
flag,就只能取得第一個 capture group、index
、input
和 groups
這些資訊:
let string = 'JavaScript ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/;
let result = string.match(pattern);
console.log(result);
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
若有 global flag 不是取得所有 capture group 和其他資訊,而是只能取得所有 match 到的字串:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let result = string.match(pattern);
console.log(result);
// ["ES7", "ES8", "ES9"]
這樣根本不夠用!這就是 String.prototype.match()
可惜的地方。
如果你只想取得所有 match 到的字串,那 String.prototype.match()
很好用,若你想要的是詳細一點的資訊,例如:capture group,String.prototype.match()
是無法滿足你的。
那改用 RegExp.prototype.exec()
呢?
RegExp.prototype.exec()
若在 RegExp.prototype.exec()
使用的 RegExp 有設定 global
或 sticky
flag,在執行 RegExp.prototype.exec()
後,會在該 RegExp
物件儲存前一個 match 的 lastIndex
(即上次最後 match 的字串的最後一個字元在原字串中的 index 為何,用於下一次 match 開始的 index)。
所以只要重複執行幾次 RegExp.prototype.exec()
,就能一直取得 match 的結果,即取得第一個 capture group、index
、input
和 groups
這些資訊。
直到 match 的結果為 null
時,代表已經找不到 match 的字串,此時會將 RegExp
物件的 lastIndex
設為 0
,代表之後執行 RegExp.prototype.exec()
會重頭開始 match 字串。
用剛剛的範例來舉例:match 到幾個字串就要跑幾次,每次都會更新 RegExp
物件的 lastIndex
:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
console.log(pattern.lastIndex);
// 0
console.log(pattern.exec(string));
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 3
console.log(pattern.exec(string));
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 7
console.log(pattern.exec(string));
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 11
console.log(pattern.exec(string));
// null
console.log(pattern.lastIndex);
// 0
若 RegExp.prototype.exec()
回傳為 null
,且 RegExp
物件的 lastIndex
為 0
時,你再次執行 RegExp.prototype.exec()
就會重頭開始 match 字串:
console.log(pattern.exec(string));
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 3
看到上面手動一步一步執行 RegExp.prototype.exec()
感到累嗎?用迴圈改寫一下:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let match;
while (match = pattern.exec(string)) {
console.log(match);
}
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
舒服多了!
若要保存每次執行 RegExp.prototype.exec()
回傳的 match 結果,可能會這樣寫:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let match;
let matches = [];
while (match = pattern.exec(string)) {
matches.push(match);
}
console.log(matches);
// [
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ]
到這邊你覺得 RegExp.prototype.exec()
還行嗎?其實有一些小缺點!
RegExp.prototype.exec()
的小缺點小缺點如下:
RegExp.prototype.exec()
會改變 RegExp
物件的 lastIndex
先來說第一個小缺點:為了取得每次 RegExp.prototype.exec()
的 match 結果,且因需要設定迴圈的中止條件,要將 match 結果存在一個變數,這個變數宣告是為了 RegExp.prototype.exec()
的行為而建立的變數 (即下面的 match
變數):
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let match;
while (match = pattern.exec(string)) {
console.log(match);
}
逼不得已啊...。那能改用 for-of
嗎?這樣就不會多宣告變數啦!
RegExp.prototype.exec()
是不可能做到的,而本篇要介紹的 String.prototype.matchAll()
就能解決這個問題,後面會提到。
接著來說第二個小缺點:執行 RegExp.prototype.exec()
會改變 RegExp
物件的 lastIndex
。
這看似沒什麼問題啊?其實問題會發生在不懂 RegExp.prototype.exec()
的人。
假設 RegExp pattern 是共用的,需要讓很多字串 match (此範例為 string1
和 string2
):
string1
使用 RegExp.prototype.exec()
string2
使用 RegExp.prototype.exec()
let string1 = 'ES7 ES8 ES9 ECMAScript';
let string2 = 'ES10 ES11 ECMAScript';
let pattern = /(ES(\d+))/g;
console.log(pattern.lastIndex);
// 0
console.log(pattern.exec(string1));
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 3
console.log(pattern.exec(string2));
// ["ES11", "ES11", "11", index: 5, input: "ES10 ES11 ECMAScript", groups: undefined]
console.log(pattern.lastIndex);
// 9
發現問題了嗎?不同字串共用的 RegExp pattern 會因為被修改了 lastIndex
,而造成拿到的結果不符合你的預期,string2
看起來是第一次用該 RegExp pattern 來 match 字串,但卻拿到第二次才會被 match 的字串 (即 ES11
,原本以為會拿到 ES10
)。
如果你熟悉
RegExp.prototype.exec()
,這個問題根本不會發生,但過去還是新手的我就採過這個雷 XD
String.prototype.replace()
有些情境需要透過 String.prototype.replace()
和 RegExp 來將某些字串取代成其他內容。
例如:將 ES7 轉成 ES2016,ES8 轉成 ES2017,ES9 轉成 ES2018,只有前綴 ES
後面加上數字字元才能轉換:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let newString = string.replace(pattern, function(matched, position1, position2) {
const version = position2;
return `ES${2009 + Number(version)}`;
});
console.log(newString);
// ES2016 ES2017 ES2018 ECMAScript
註:
String.prototype.replace()
的第二個參數可以是字串,或是 callback function,而 callback function 會有多個參數,包括:matched
(match 到的字串)、positionN
(第幾個 capture group)、index
(match 到字元的 index) 和input
(正在 match 的整個字串)。string.replace(pattern, function(matched, position1, ...positionN, index, input) { // ... });
若要讓 String.prototype.replace()
的行為很像 RegExp.prototype.exec()
回傳的 match 結果,可以這樣寫:
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let matches = [];
let newString = string.replace(pattern, function() {
const match = [...arguments].slice(0, -2);
match.input = arguments[arguments.length - 1];
match.index = arguments[arguments.length - 2];
matches.push(match);
return `ES${2009 + Number(match[2])}`;
});
console.log(newString);
// ES2016 ES2017 ES2018 ECMAScript
console.log(matches);
// [
// ["ES7", "ES7", "7", input: "ES7 ES8 ES9 ECMAScript", index: 0],
// ["ES8", "ES8", "8", input: "ES7 ES8 ES9 ECMAScript", index: 4],
// ["ES9", "ES9", "9", input: "ES7 ES8 ES9 ECMAScript", index: 8]
// ]
但太麻煩了...
String.prototype.matchAll()
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let matches = string.matchAll(pattern);
console.log(matches);
// RegExpStringIterator {}
for (const match of matches) {
console.log(match);
console.log(`lastIndex: ${pattern.lastIndex}`);
}
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// lastIndex: 0
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// lastIndex: 0
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// lastIndex: 0
let string = 'ES7 ES8 ES9 ECMAScript';
let pattern = /(ES(\d+))/g;
let matches = [...string.matchAll(pattern)];
console.log(matches);
// [
// ["ES7", "ES7", "7", index: 0, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES8", "ES8", "8", index: 4, input: "ES7 ES8 ES9 ECMAScript", groups: undefined],
// ["ES9", "ES9", "9", index: 8, input: "ES7 ES8 ES9 ECMAScript", groups: undefined]
// ]
RegExp
) | JavaScript for impatient programmers (ES2020 edition)