JavaScript: 正規表現/gと名前付きグループを併用する小技


本稿では、JavaScriptの正規表現の/gと名前付きグループ(?<name>...)を併用する小技を紹介します。

名前付きグループとは

名前付きグループとは、正規表現のグループ(...)に名前をつけ、グループにマッチした文字列をインデックスではなく、その名前で取り出せるようになるものです。

// 名前がついてないグループ
const result1 = '[email protected]'.match(/@(.+)/)
console.log(result1[1]) //=> example.com

// 名前付きグループ
const result2 = '[email protected]'.match(/@(?<domain>.+)/)
console.log(result2.groups.domain) //=> example.com

/gと名前付きグループは併用しにくい

/gと名前付きグループを組み合わせると、matchでは.groupsで名前付きグループを参照することができなくなります:

const result1 = '123'.match(/(\d)/g)
console.log(result1) //=> [ '1', '2', '3' ]

const result2 = '123'.match(/(?<digit>\d)/g)
console.log(result2) //=> [ '1', '2', '3' ]

RegExp.prototype.execを使うと、/g.groupsによる参照を両立できますが、コードがごちゃつきます:

const regexp = /(?<digit>\d)/g
let match
const result3 = []
while ((match = regexp.exec('123')) !== null) {
  result3.push(match.groups)
}
console.log(result3)
// => [
//  { digit: '1' },
//  { digit: '2' },
//  { digit: '3' }
// ]

ECMAScript 2019で導入された、String.prototype.matchAllを使うと、コードはきれいになります:


const result4 = '123'.matchAll(/(?<digit>\d)/g)
const result5 = [...result4].map(match => match.groups)
console.log(result5)
// => [
//  { digit: '1' },
//  { digit: '2' },
//  { digit: '3' }
// ]

しかし、ECMAScript 2019に対応したJavaScript実行環境でなければこのメソッドは使えません。

(古い環境で)正規表現/gと名前付きグループを併用する小技

ECMAScript 2019未満の古い環境で、ポリフィルなどなしに、String.prototype.matchAllのような比較的ととのった見た目のコードで、正規表現/gと名前付きグループを両立するにはどうしたらいいでしょうか。

少しトリッキーですが、String.prototype.replaceを使います:

const string = '123'
const regexp = /(?<digit>\d)/g

const result = []
string.replace(regexp, (...args) => result.push(args.pop()))

console.log(result)
// => [
//  { digit: '1' },
//  { digit: '2' },
//  { digit: '3' }
// ]

String.prototype.replaceは第2引数にコールバック関数を渡すことができます。コールバック関数の引数には、マッチした文字列の情報がいろいろ渡されますが、最後の引数がgroupsなので、最後のその引数.pop()すると、名前付きグループのオブジェクトが取れるというわけです。

おわりに

今回紹介した小技はある種のハックなので、String.prototype.matchAllが使えるようにできないかを検討するのが先です。