[JavaScript]String.matchとRegExp.execと後方参照

Adobeのサポートデータベース関係のぐりもんを書いたり添削してもらったりした結果、新たに覚えたことがあるので、せっかくだからメモっておきます。
以下だらだらと続くけど、実はMozilla Developer Center見れば全部書いてあるので、そっち見たほうがいいと思います。
match - MDC
exec - MDC

String.matchとRegExp.exec

String.matchとRegExp.execは、どちらも正規表現を使って文字列のマッチングを行うメソッド。
検索を行って、マッチした文字列を得たいときに使う。マッチするかどうか(と、マッチした位置)だけを調べたいときは、余分な処理をしないString.searchまたはRegExp.testを使ったほうが速い。

String.match(RegExp)とRegExp.exec(String)は、正規表現にgオプションがついてないときは同じ動作をする。

gオプションなしの場合の動作(String.match/RegExp.exec共通)

返り値は配列になる。配列の中身は

[0] : 最後にマッチした文字列全体
[1] : キャプチャされた部分文字列1番目(RegExp.$1と一致)
[2] : キャプチャされた部分文字列2番目(RegExp.$2と一致)
・
・
・
[n] : キャプチャされた部分文字列n番目(RegExp.$nと一致)

capturing parentheses、キャプチャされるグループ? 後方参照? 言い方がいろいろあってよくわからない。とにかくマッチした文字列の中で、あとから部分的に取り出すことができるように保存する。
RegExpのプロパティだと$9までになるけど、この方法だと部分文字列の数に制限はないらしい。
マッチする文字列が無かった場合は、nullを返す。
今までこういう形で配列に格納されることを知らなかったので、いちいちRegExp.$1にアクセスしてたよ……。

var str = "ABCDEFGHIJKLMN";
var regexp = /([A-N])([A-N]{2})[A-N]/;

var match_result = str.match(regexp); // ["ABCD", "A", "BC"]
var exec_result  = regexp.exec(str);  // ["ABCD", "A", "BC"]

どちらも ["ABCD", "A", "BC"]の配列になる。
それぞれマッチング直後のRegExp.$1を調べると、どちらも"A"を返す。
つまり1つ目のグループの文字列を得たいときは、match_result[1]/exec_result[1]/RegExp.$1のどれかにアクセスすればおk。この場合は配列を変数に格納してるので、それを使ったほうがいい。

gオプションありの場合の動作(String.match)

返り値は、マッチしたすべての文字列全体の配列。と書くとたいへんわかりづらい……。カモン文章力。
要するに、部分文字列は配列に含まれないということ。

var str = "ABCDEFGHIJKLMN";
var regexp = /([A-N])([A-N]{2})[A-N]/g;

var match_result = str.match(regexp);

マッチするたびに「次の検索開始位置」がregexp.lastIndexに格納され、マッチするものがなくなるまで続く。結果は ["ABCD", "EFGH", "IJKL"]の配列になる。
ただしもちろん部分文字列の保存は行われるので、直後にRegExp.$1を調べると"I"を返す(最後に行われたマッチングの分が格納されている)。

同じregexpオブジェクトを使ってマッチングを繰り返した場合、すべて同じ結果になる。

var str = "ABCDEFGHIJKLMN";
var regexp = /([A-N])([A-N]{2})[A-N]/g;

var match_result1 = str.match(regexp); // ["ABCD", "EFGH", "IJKL"]
var match_index1 = regexp.lastIndex;   // 0

var match_result2 = str.match(regexp); // ["ABCD", "EFGH", "IJKL"]
var match_index2 = regexp.lastIndex;   // 0

var match_result3 = str.match(regexp); // ["ABCD", "EFGH", "IJKL"]
var match_index3 = regexp.lastIndex;   // 0

マッチに失敗した場合はnullが返る。

gオプションありの場合の動作(Regexp.exec)

返り値は、最初にマッチした文字列全体+キャプチャした文字列の配列。ただし、一度実行すると、正規表現オブジェクトに「次回検索開始位置」が設定される(lastIndexプロパティ)。
なので、同じオブジェクトでマッチングを繰り返すと、そのたびに返り値は変わる。

var str = "ABCDEFGHIJKLMN";
var regexp = /([A-N])([A-N]{2})[A-N]/g;

var exec_result1 = regexp.exec(str); // ["ABCD", "A", "BC"]
var exec_index1 = regexp.lastIndex;  // 4

var exec_result2 = regexp.exec(str); // ["EFGH", "E", "FG"]
var exec_index2 = regexp.lastIndex;  // 8

var exec_result3 = regexp.exec(str); // ["IJKL", "I", "JK"]
var exec_index3 = regexp.lastIndex;  // 12

lastIndexだけ更新して、あとはgフラグないときと同じように動くって考えればいいのかな。
マッチに失敗したらlastIndexを0に戻して、nullを返す。

これを利用すると、こんな風に処理を繰り返せる。

var str = "ABCDEFGHIJKLMN";
var regexp = /([A-N])([A-N]{2})[A-N]/g;

var m = [];
while (m = regexp.exec(str)) {
  alert(m[2]);
}

「BC」「FG」「JK」と表示されます。
……もうちょっと実用的な例にするべきだった気がする。

で、つまり

何が言いたかったかというと、こないだのぐりもんのここの部分(11〜13行目)。
最新版じゃないけど気にしない。

11:  var m = location.search.match(/^\?(\d{6})\+/);
12:  if (!m) return;
13:  var doc_number = m[1]; 

これはCodeReposの方でdrry氏が書きなおしてくれた部分(の一部)。その前はこう書いてた↓

11:  location.href.match(/\?(\d{6})\+/);
12:  var doc_number = RegExp.$1;

修正版を見たときどうしてこう書けるのかわからなくて、上記のことを調べてやっと理解したのでした。
String.matchが「マッチした文字列+キャプチャした文字列」の配列を返すことを利用して、RegExp.$1へのアクセスを省略。加えて、マッチしなかった場合はnullを返すので、文書番号が6桁のパターンに当てはまらなかった場合はその時点で処理を中断する、というやり方。
たいへん勉強になりました。

隅っこでこっそりとはいえ、何か書いて人に見てもらえるっていいことだなあというお話でした。