Skip to content

Commit b804895

Browse files
authored
feat(string): String#matchAllに対応、RegExp#execをコラムに移動 (#1250)
* chore(string): アウトラインの更新 * feat(operator): matchAllベースに書き換え * fix * fix * fix * fix * fix * fix * fix * update * fix link * fix * fix * fix * fix * fix
1 parent 53c2ccd commit b804895

File tree

2 files changed

+163
-56
lines changed

2 files changed

+163
-56
lines changed

source/basic/string/OUTLINE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
- 正規表現による検索
3232
- インデックス
3333
- マッチ
34+
- String#match
35+
- String#match with /g/
36+
- String#matchでは indexが取れない
37+
- [ES2020] String#matchAll
38+
- マッチ + キャプチャ
39+
- String#matchAll
40+
- コラム
41+
- RegExp#execでString#matchAllを再現していた理由
3442
- 真偽値
3543
- 文字列と正規表現
3644
- 文字列の置換

source/basic/string/README.md

Lines changed: 155 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ console.log(str.includes("いる")); // => true
527527

528528
正規表現は正規表現オブジェクト(`RegExp`オブジェクト)として表現されます。
529529
正規表現オブジェクトはマッチする範囲を決める`パターン`と正規表現の検索モードを指定する`フラグ`の2つで構成されます。
530-
正規表現のパターン内では、次の文字は**特殊文字**と呼ばれ、特別な意味を持ちます。特殊文字として解釈されないように入力する場合には`\`(バックスラッシュ)でエスケープすることが必要です
530+
正規表現のパターン内では、次の文字は**特殊文字**と呼ばれ、特別な意味を持ちます。特殊文字として解釈されないように入力する場合には`\`(バックスラッシュ)でエスケープする必要があります
531531

532532
```
533533
\ ^ $ . * + ? ( ) [ ] { } |
@@ -655,23 +655,32 @@ const index = str.search(searchPattern); // => 3
655655
str.slice(index, index + マッチした文字列の長さ); // マッチした文字列は取得できない
656656
```
657657

658-
そのため、マッチした文字列を取得する`RegExp#exec`メソッドと`String#match`メソッドが用意されています。
659-
これらのメソッドは、正規表現のマッチを文字列の最後まで繰り返す`g`フラグ(globalの略称)と組み合わせてよく利用されます。
660-
また、`g`フラグの有無によって返り値が変わるのも特徴的です。
658+
そのため、マッチした文字列を取得する`String#match`メソッドと`String#matchAll`メソッドが用意されています。
659+
また、これらのメソッドは正規表現のマッチを文字列の最後まで繰り返す`g`フラグ(globalの略称)によって挙動が変わります。
661660

662-
- `String#match(正規表現)`: 文字列中でマッチするものを検索する
663-
- マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
664-
- マッチしない場合は、`null`を返す
665-
- 正規表現の`g`フラグが有効化されているときは、マッチしたすべての結果を含んだ配列を返す
666-
- `RegExp#exec(文字列)`: 文字列中でマッチするものを検索する
667-
- マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
668-
- マッチしない場合は、`null`を返す
669-
- 正規表現の`g`フラグが有効化されているときは、マッチした末尾のインデックスを`lastIndex`プロパティに記憶する
661+
##### マッチした文字列の取得 {#match}
670662

671-
`String#match`メソッドは正規表現の`g`フラグなしのパターンで検索した場合、マッチしたものが見つかった時点で検索が終了します。
672-
このときの`match`メソッドの返り値である配列は`index`プロパティと`input`プロパティが追加された特殊な配列となります。
663+
まずは、マッチした文字列を取得する`String#match`メソッドから見ていきます。
664+
`String#match`メソッドは、正規表現の`/パターン/``"文字列"`にマッチすると、マッチした文字列に関する情報を返すメソッドです。
665+
666+
```js
667+
"文字列".match(/パターン/);
668+
```
669+
670+
`String#match`メソッドで検索した結果、正規表現にマッチする文字列がなかった場合は`null`を返します。
671+
672+
{{book.console}}
673+
```js
674+
console.log("文字列".match(/マッチしないパターン/)); // => null
675+
```
676+
677+
`String#match`メソッドは正規表現の`g`フラグなしのパターンで検索した場合、最初にマッチしたものが見つかった時点で検索が終了します。
678+
このときの`match`メソッドの返り値は、`index`プロパティと`input`プロパティをもった特殊な配列となります。
679+
`index`プロパティにはマッチした文字列の先頭のインデックスが、`input`プロパティには検索対象となった文字列全体が含まれています。
673680

674681
次のコードの`/[a-zA-Z]+/`という正規表現は`a`から`Z`のどれかの文字が1つ以上連続しているものにマッチします。
682+
この正規表現にマッチした文字列は、返り値の配列からインデックスアクセスで取得できます。
683+
`g`フラグなしでは、最初にマッチしたものを見つけた時点で検索が終了するので、返り値の配列には1つの要素しか含まれていません。
675684

676685
{{book.console}}
677686
```js
@@ -688,10 +697,10 @@ console.log(results.index); // => 0
688697
console.log(results.input); // => "ABC あいう DE えお"
689698
```
690699

691-
`String#match`メソッドは正規表現の`g`フラグありのパターンで検索した場合、マッチしたすべての結果を含んだ配列を返します
700+
`String#match`メソッドは正規表現の`g`フラグありのパターンで検索した場合、マッチしたすべての文字列を含んだ配列を返します
692701

693702
次のコードの`/[a-zA-Z]+/g`という正規表現は`a`から`Z`のどれかの文字が1つ以上連続しているものに繰り返しマッチします。
694-
このパターンにマッチする箇所は2つあるため`String#match`メソッドの返り値である配列にも2つの要素が含まれています。
703+
この正規表現にマッチする箇所は"ABC"と"DE"の2つとなるため`String#match`メソッドの返り値である配列にも2つの要素が含まれています。
695704

696705
{{book.console}}
697706
```js
@@ -707,7 +716,111 @@ console.log(resultsWithG.index); // => undefined
707716
console.log(resultsWithG.input); // => undefined
708717
```
709718

710-
`RegExp#exec`メソッドも、`g`フラグの有無によって挙動が変化します。
719+
このときの`match`メソッドの返り値である配列には`index``input`プロパティはありません。
720+
なぜなら、複数の箇所にマッチする場合においては、1つの`index`プロパティでは意味が一意に決まらないためです。
721+
722+
`String#match`メソッドの挙動をまとめると次のようになります。
723+
724+
- マッチしない場合は、`null`を返す
725+
- マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
726+
- 正規表現の`g`フラグがある場合は、マッチしたすべての結果を含んだただの配列を返す
727+
728+
<!--
729+
RegExp#match globalがtrueの場合はプロパティがないただの配列を返す
730+
https://tc39.es/ecma262/#sec-regexp.prototype-@@match
731+
RegExp#match globalがfalseの場合はString#execと同じ
732+
-->
733+
734+
ES2020では、正規表現の`g`フラグを使った繰り返しマッチする場合においても、それぞれマッチした文字列ごとの情報を得るための`String#matchAll`が追加されています。
735+
`String#matchAll`メソッドは、マッチした結果をIteratorで返します。
736+
737+
次のコードでは、`matchAll`メソッドでアルファベットにマッチする結果のIteratorオブジェクトを取得しています。
738+
Iterratorオブジェクトは`for...of`構文で反復処理すると、Iteratorから値を1つずつ取り出して処理できます(詳細は「[ループと反復処理][]」の章を参照)。
739+
このときの反復処理で取得できる値は、それぞれのマッチした文字列と`index``input`プロパティを持つ特殊な配列となります。
740+
741+
{{book.console}}
742+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
743+
```js
744+
const str = "ABC あいう DE えお";
745+
const alphabetsPattern = /[a-zA-Z]+/g;
746+
// matchAllはIteratorを返す
747+
const matchesIterator = str.matchAll(alphabetsPattern);
748+
for (const match of matchesIterator) {
749+
// マッチした要素ごとの情報を含んでいる
750+
console.log(`match: "${match[0]}", index: ${match.index}, input: "${match.input}"`);
751+
}
752+
// 次の順番でコンソールに出力される
753+
// match: "ABC", index: 0, input: "ABC あいう DE えお"
754+
// match: "DE", index: 8, input: "ABC あいう DE えお"
755+
```
756+
757+
そのため、正規表現の`g`フラグを使った繰り返しマッチを行う場合には、`match`メソッドではなく`matchAll`メソッドを利用します。
758+
また、`matchAll`メソッドは`g`フラグなしの正規表現はサポートしていないため、`g`フラグなしの正規表現を渡した場合は例外が発生します。
759+
760+
#### マッチした文字列の一部を取得 {#match-capture-by-regexp}
761+
762+
`String#match`メソッドと`String#matchAll`メソッドは、どちらも正規表現のキャプチャリングに対応しています。
763+
キャプチャリングとは、正規表現中で`/パターン1(パターン2)/`のようにカッコで囲んだ部分を取り出すことです。
764+
このキャプチャリングによって、正規表現でマッチした一部分だけを取り出せます。
765+
766+
`String#match`メソッドと`String#matchAll`メソッドはどちらもマッチした結果を配列として返します。
767+
768+
そのマッチしているパターンにキャプチャが含まれている場合は、返り値の配列へキャプチャした部分が追加されていきます。
769+
配列の先頭にはマッチした文字列全体が入り、順番にキャプチャリング(`(``)`)で囲んだ範囲が配列に含まれます。
770+
771+
<!-- doctest:disable -->
772+
```js
773+
const [マッチした全体の文字列, キャプチャ1, キャプチャ2] = 文字列.match(/パターン(キャプチャ1)と(キャプチャ2)/);
774+
```
775+
776+
次のコードでは、`ECMAScript 数字``数字`部分だけを取り出そうとしています。
777+
`String#match`メソッドとキャプチャリングによって数字(`\d+`)にマッチする部分を取り出しています。
778+
779+
{{book.console}}
780+
```js
781+
// "ECMAScript (数字+)"にマッチするが、欲しい文字列は数字の部分のみ
782+
const pattern = /ECMAScript (\d+)/;
783+
// 返り値は0番目がマッチした全体、1番目がキャプチャの1番目というように対応している
784+
// [マッチした全部の文字列, キャプチャの1番目, キャプチャの2番目 ....]
785+
const [all, capture1] = "ECMAScript 6".match(pattern);
786+
console.log(all); // => "ECMAScript 6"
787+
console.log(capture1); // => "6"
788+
```
789+
790+
正規表現の`g`フラグを使い繰り返し文字列にマッチする場合には、`String#matchAll`メソッドを利用します。
791+
先ほども紹介したように、`String#match`メソッドは繰り返しマッチした場合に、それぞれ個別のマッチした情報を取得できないためです。
792+
793+
次のコードでは、`ES数字`の数字(`\d+`)にマッチする部分を取り出しています。
794+
`String#matchAll`の返り値であるIteratorを反復処理することで、それぞれマッチしたキャプチャを取り出しています。
795+
796+
{{book.console}}
797+
<!-- doctest:meta:{ "ECMAScript": 2020 } -->
798+
```js
799+
// "ES(数字+)"にマッチするが、欲しい文字列は数字の部分のみ
800+
const pattern = /ES(\d+)/g;
801+
// iteratorを返す
802+
const matchesIterator = "ES2015、ES2016、ES2017".matchAll(pattern);
803+
for (const match of matchesIterator) {
804+
// マッチした要素ごとの情報を含んでいる
805+
// 0番目はマッチした文字列全体、1番目がキャプチャの1番目である数字
806+
console.log(`match: "${match[0]}", capture1: ${match[1]}, index: ${match.index}, input: "${match.input}"`);
807+
}
808+
// 次の順番でコンソールに出力される
809+
// match: "ES2015", capture1: 2015, index: 0, input: "ES2015、ES2016、ES2017"
810+
// match: "ES2016", capture1: 2016, index: 7, input: "ES2015、ES2016、ES2017"
811+
// match: "ES2017", capture1: 2017, index: 14, input: "ES2015、ES2016、ES2017"
812+
```
813+
814+
#### [コラム] RegExp#execでのString#matchAll {#regexp-exec}
815+
816+
`String#matchAll`メソッドは、ES2020で導入されたメソッドです。
817+
それまでは、`RegExp#exec`メソッドという`String#match`メソッドによく似た挙動をするメソッドを利用して、`String#matchAll`メソッド相当の表現を実装していました。
818+
819+
`RegExp#exec`メソッドは、引数に文字列を受け取るメソッドです。
820+
821+
```js
822+
/pattern/.exec("文字列");
823+
```
711824

712825
`RegExp#exec`メソッドは`g`フラグなしのパターンで検索した場合、マッチした最初の結果のみを含む特殊な配列を返します。
713826
このときの`exec`メソッドの返り値である配列が`index`プロパティと`input`プロパティが追加された特殊な配列となるのは、`String#match`メソッドと同様です。
@@ -728,8 +841,8 @@ console.log(results.input); // => "ABC あいう DE えお"
728841

729842
`RegExp#exec`メソッドは`g`フラグありのパターンで検索した場合も、マッチした最初の結果のみを含む特殊な配列を返します。
730843
この点は`String#match`メソッドとは異なります。
731-
また、最後にマッチした末尾のインデックスを正規表現オブジェクトの`lastIndex`プロパティに記憶します
732-
そしてもう一度`exec`メソッドを呼び出すと最後にマッチした末尾のインデックスから検索が開始されます
844+
また、最後にマッチした文字列末尾のインデックスを正規表現オブジェクトの`lastIndex`プロパティに記録します
845+
そしてもう一度`exec`メソッドを呼び出すと最後にマッチした末尾のインデックス(`lastIndex`プロパティの位置)から検索が開始されます
733846

734847
{{book.console}}
735848
```js
@@ -751,54 +864,39 @@ console.log(result3); // => null
751864
console.log(alphabetsPattern.lastIndex); // => 0
752865
```
753866

754-
この`lastIndex`プロパティが検索ごとに更新される仕組みを利用することで、`exec`を反復処理してすべての検索結果を取得できます。
755-
`exec`メソッドはマッチしなければ`null`を返すため、マッチするものがなくなればwhile文から自動的に脱出します。
867+
`RegExp#exec`メソッドの挙動をまとめると次のようになります。
868+
正規表現の`g`フラグがない場合は、`String#match`メソッドと同じ結果です。
869+
一方で、正規表現の`g`フラグがある場合は、`String#match`メソッドとは異なる挙動をします。
870+
871+
- マッチしない場合は、`null`を返す
872+
- マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
873+
- 正規表現の`g`フラグがある場合は、マッチした文字列を含んだ特殊な配列を返し、マッチした末尾のインデックスを正規表現オブジェクトの`lastIndex`プロパティに記録する
874+
875+
この正規表現の`g`フラグと`exec`メソッドで検索した場合に、`lastIndex`プロパティが検索ごとに更新される仕組みを利用して、マッチするすべての結果を取得できます。
876+
877+
次のコードでは、`RegExp#exec`メソッド使いアルファベットにマッチした結果を`matches`に保持しています。
878+
`g`フラグがある場合の`exec`メソッドでは最後にマッチした位置が記録されているため、`while`文で反復処理して続きから検索しています。
879+
また、`exec`メソッドはマッチしなければ`null`を返すため、マッチするものがなくなればwhile文から自動的に脱出します。
756880

757881
{{book.console}}
758882
```js
759883
const str = "ABC あいう DE えお";
760884
const alphabetsPattern = /[a-zA-Z]+/g;
761885
let matches;
762886
while (matches = alphabetsPattern.exec(str)) {
763-
console.log(`match: ${matches[0]}, lastIndex: ${alphabetsPattern.lastIndex}`);
887+
// `RegExp#exec`メソッドの返り値は`index`プロパティなどを含む特殊な配列
888+
console.log(`match: ${matches[0]}, index: ${matches.index}, lastIndex: ${alphabetsPattern.lastIndex}`);
764889
}
765-
// コンソールには次のように出力される
766-
// match: ABC, lastIndex: 3
767-
// match: DE, lastIndex: 10
768-
```
769-
770-
このように`String#match`メソッドと`RegExp#exec`メソッドはどちらも`g`フラグによって挙動が変わります。
771-
また`RegExp#exec`メソッドは、正規表現オブジェクトの`lastIndex`プロパティを変更するという副作用を持ちます。
772-
773-
#### マッチした一部の文字列を取得 {#match-capture-by-regexp}
774-
775-
`String#match`メソッドと`RegExp#exec`メソッドのどちらも正規表現のキャプチャリングに対応しています。
776-
キャプチャリングとは、正規表現中で`/パターン1(パターン2)/`のようにカッコで囲んだ部分を取り出すことです。
777-
このキャプチャリングによって、正規表現でマッチした一部分だけを取り出せます。
778-
779-
`String#match`メソッド、`RegExp#exec`メソッドのどちらもマッチした結果を配列として返します。
780-
781-
そのマッチしているパターンにキャプチャが含まれている場合は、次のように返り値の配列へキャプチャした部分が追加されていきます。
782-
783-
<!-- doctest:disable -->
784-
```js
785-
const [マッチした全体の文字列, ...キャプチャされた文字列] = 文字列.match(/パターン(キャプチャ)/);
890+
// 次の順番でコンソールに出力される
891+
// match: ABC, index: 0, lastIndex: 3
892+
// match: DE, index: 8, lastIndex: 10
786893
```
787894

788-
次のコードでは、`ECMAScript 数字``数字`部分だけを取り出そうとしています
789-
`String#match`メソッドとキャプチャリングによって数字(`\d`)にマッチする部分を取り出しています
895+
このように`RegExp#exec`メソッドと正規表現の`g`フラグを使い、`String#matchAll`メソッド相当の反復処理を実装していました
896+
`RegExp#exec`はIteratorオブジェクトという反復処理のためオブジェクトが導入される以前からあるメソッドです
790897

791-
{{book.console}}
792-
```js
793-
// "ECMAScript (数字+)"にマッチするが、欲しい文字列は数字の部分のみ
794-
const pattern = /ECMAScript (\d+)/;
795-
// 返り値は0番目がマッチした全体、1番目がキャプチャの1番目というように対応している
796-
// [マッチした全部の文字列, キャプチャの1番目, キャプチャの2番目 ....]
797-
// `pattern.exec("ECMAScript 6")`も返り値は同じ
798-
const [all, capture1] = "ECMAScript 6".match(pattern);
799-
console.log(all); // => "ECMAScript 6"
800-
console.log(capture1); // => "6"
801-
```
898+
`String#matchAll`がIteratorを扱うわかりやすい反復処理に比べて、`RegExp#exec`メソッドは`while`文などで手動で反復処理を書く必要があるため直感的ではありません。
899+
そのため、`String#matchAll`メソッドが利用できる場合に、`RegExp#exec`メソッドを利用する必要はありません。
802900

803901
#### 真偽値を取得 {#test-by-regexp}
804902

@@ -1145,6 +1243,7 @@ console.log(escapedURL); // => "https://example.com/search?q=A%26B&sort=desc"
11451243

11461244

11471245
[文字列とUnicode]: ../string-unicode/README.md
1246+
[ループと反復処理]: ../loop/README.md
11481247
[エスケープシーケンス]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String#%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9
11491248
[MDNの正規表現ドキュメント]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions "正規表現 - JavaScript | MDN"
11501249
[regex101]: https://regex101.com/ "Online regex tester and debugger: PHP, PCRE, Python, Golang and JavaScript"

0 commit comments

Comments
 (0)