文字列
この章ではJavaScriptにおける文字列について学んでいきます。まずは、文字列の作成方法や文字列の操作方法について見ていきます。そして、文字列を編集して自由に文字列を作れるようになることがこの章の目的です。
文字列を作成する
文字列を作成するには、文字列リテラルを利用します。「データ型とリテラル」の章でも紹介しましたが、文字列リテラルには"
(ダブルクォート)、'
(シングルクォート)、`
(バッククォート)の3種類があります。
まずは"
(ダブルクォート)と'
(シングルクォート)について見ていきます。
"
(ダブルクォート)と'
(シングルクォート)に意味的な違いはありません。そのため、どちらを使うかは好みやプロジェクトごとのコーディング規約によって異なります。この書籍では、"
(ダブルクォート)を主な文字列リテラルとして利用します。
const double ="文字列";console.log(double);// => "文字列"const single ='文字列';console.log(single);// => '文字列'// どちらも同じ文字列console.log(double === single);// => true
ES2015では、テンプレートリテラル`
(バッククォート)が追加されました。`
(バッククォート)を利用することで文字列を作成できる点は、他の文字列リテラルと同じです。
これに加えてテンプレートリテラルでは、文字列中に改行を入力できます。次のコードでは、テンプレートリテラルを使って複数行の文字列を見た目どおりに定義しています。
const multiline =`1行目2行目3行目`;// \n は改行を意味するconsole.log(multiline);// => "1行目\n2行目\n3行目"
どの文字列リテラルでも共通ですが、文字列リテラルは同じ記号が対になります。そのため、文字列の中にリテラルと同じ記号が出現した場合は、\
(バックスラッシュ)を使いエスケープする必要があります。次のコードでは、文字列中の"
を\"
のようにエスケープしています。
const str ="This book is \"js-primer\"";console.log(str);// => 'This book is "js-primer"'
エスケープシーケンス
文字列リテラル中にはそのままでは入力できない特殊な文字もあります。改行もそのひとつで、"
(ダブルクォート)と'
(シングルクォート)の文字列リテラルには改行をそのまま入力できません(テンプレートリテラル中には例外的に改行をそのまま入力できます)。
次のコードは、JavaScriptの構文として正しくないため、構文エラー(SyntaxError)となります。
// JavaScriptエンジンが構文として解釈できないため、SyntaxErrorとなるconst invalidString ="1行目2行目3行目";
この問題を回避するためには、改行のような特殊な文字をエスケープシーケンスとして書く必要があります。エスケープシーケンスは、\
と特定の文字を組み合わせることで、特殊文字を表現します。
次の表では、代表的なエスケープシーケンスを紹介しています。エスケープシーケンスは、"
(ダブルクォート)、'
(シングルクォート)、`
(バッククォート)すべての文字列リテラルの中で利用できます。
エスケープシーケンス | 意味 |
---|---|
\' | シングルクォート |
\" | ダブルクォート |
\` | バッククォート |
\\ | バックスラッシュ(\ そのものを表示する) |
\n | 改行 |
\t | タブ |
\uXXXX | Code Unit(\u と4桁のHexDigit) |
\u{X} ...\u{XXXXXX} | Code Point(\u{} のカッコ中にHexDigit) |
このエスケープシーケンスを利用することで、先ほどの"
(ダブルクォート)の中に改行(\n
)を入力できます。
// 改行を\nのエスケープシーケンスとして入力しているconst multiline ="1行目\n2行目\n3行目";console.log(multiline);/* 改行した結果が出力される1行目2行目3行目*/
また、\
からはじまる文字は自動的にエスケープシーケンスとして扱われます。しかし、\a
のように定義されていないエスケープシーケンスは、\
が単に無視されa
という文字列として扱われます。これにより、\
(バックスラッシュ)そのものを入力していたつもりが、その文字がエスケープシーケンスとして扱われてしまう問題があります。
次のコードでは、\_
という組み合わせのエスケープシーケンスはないため、\
が無視された文字列として評価されます。
console.log("¯\_(ツ)_/¯");// ¯_(ツ)_/¯ のように\が無視されて表示される
\
(バックスラッシュ)そのものを入力したい場合は、\\
のようにエスケープする必要があります。
console.log("¯\\_(ツ)_/¯");// ¯\_(ツ)_/¯ と表示される
文字列を結合する
文字列を結合する簡単な方法は文字列結合演算子(+
)を使う方法です。
const str ="a" +"b";console.log(str);// => "ab"
変数と文字列を結合したい場合も文字列結合演算子で行えます。
const name ="JavaScript";console.log("Hello " + name +"!");// => "Hello JavaScript!"
特定の書式に文字列を埋め込むには、テンプレートリテラルを使うとより宣言的に書けます。
テンプレートリテラル中に${変数名}
で書かれた変数は評価時に展開されます。つまり、先ほどの文字列結合は次のように書けます。
const name ="JavaScript";console.log(`Hello${name}!`);// => "Hello JavaScript!"
文字へのアクセス
文字列の特定の位置にある文字にはインデックスを指定してアクセスできます。これは、配列における要素へのアクセスにインデックスを指定するのと同じです。
文字列では文字列[インデックス]
のように指定した位置(インデックス)の文字へアクセスできます。インデックスの値は0
以上2^53 - 1
未満の整数が指定できます。
const str ="文字列";// 配列と同じようにインデックスでアクセスできるconsole.log(str[0]);// => "文"console.log(str[1]);// => "字"console.log(str[2]);// => "列"
また、存在しないインデックスへのアクセスでは配列やオブジェクトと同じようにundefined
を返します。
const str ="文字列";// 42番目の要素は存在しないconsole.log(str[42]);// => undefined
[ES2022]String.prototype.at
ES2022からString.prototype.at
メソッドが追加されています。Stringのat
メソッドは、Arrayのat
メソッドと同じく、相対的なインデックスを渡してその位置の文字へアクセスできます。at
メソッドへ-1
のようにマイナスのインデックスを渡した場合は、末尾から数えた位置の文字へアクセスできます。
const str ="文字列";console.log(str.at(0));// => "文"console.log(str.at(1));// => "字"console.log(str.at(2));// => "列"console.log(str.at(-1));// => "列"
文字列とは
今まで何気なく「文字列」という言葉を利用していましたが、ここでいう文字列とはどのようなものでしょうか? コンピュータのメモリ上に文字列の「ア」といった文字をそのまま保存できないため、0と1からなるビット列へ変換する必要があります。この文字からビット列へ変換することを符号化(エンコード)と呼びます。
一方で、変換後のビット列が何の文字なのかを管理する表が必要になります。この文字に対応するIDの一覧表のことを符号化文字集合と呼びます。
次の表は、Unicodeという文字コードにおける符号化文字集合からカタカナの一部分を取り出したものです。1Unicodeはすべての文字に対してID(Code Point)を振ることを目的に作成されている仕様です。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
30A0 | ゠ | ァ | ア | ィ | イ | ゥ | ウ | ェ | エ | ォ | オ | カ | ガ | キ | ギ | ク |
30B0 | グ | ケ | ゲ | コ | ゴ | サ | ザ | シ | ジ | ス | ズ | セ | ゼ | ソ | ゾ | タ |
30C0 | ダ | チ | ヂ | ッ | ツ | ヅ | テ | デ | ト | ド | ナ | ニ | ヌ | ネ | ノ | ハ |
JavaScript(ECMAScript)は文字コードとしてUnicodeを採用し、文字をエンコードする方式としてUTF-16を採用しています。UTF-16とは、それぞれの文字を16ビットのビット列に変換するエンコード方式です。Unicodeでは1文字を表すのに使う最小限のビットの組み合わせをCode Unit(符号単位)と呼び、UTF-16では各Code Unitのサイズが16ビット(2バイト)です。
次のコードは、文字列を構成するCode Unitをhex値(16進数)にして表示する例です。StringのcharCodeAt
メソッドは、文字列の指定インデックスのCode Unitを整数として返します。そのCode Unitの整数値をNumberのtoString
メソッドでhex値(16進数)にしています。
const str ="アオイ";// それぞれの文字をCode Unitのhex値(16進数)に変換する// toStringの引数に16を渡すと16進数に変換されるconsole.log(str.charCodeAt(0).toString(16));// => "30a2"console.log(str.charCodeAt(1).toString(16));// => "30aa"console.log(str.charCodeAt(2).toString(16));// => "30a4"
逆に、Code Unitをhex値(16進数)から文字へと変換するにはString.fromCharCode
メソッドを使います。次のコードでは、16進数の整数リテラルである0x
で記述したCode Unitから文字列へと変換しています(0x
リテラルについては「データ型とリテラル」の章を参照)。
const str =String.fromCharCode(0x30a2,// アのCode Unit0x30aa,// オのCode Unit0x30a4// イのCode Unit);console.log(str);// => "アオイ"
これらの結果をまとめると、この文字列と文字列を構成するUTF-16のCode Unitとの関係は次のようになります。
インデックス | 0 | 1 | 2 |
---|---|---|---|
文字列 | ア | オ | イ |
UTF-16のCode Unit(16進数) | 0x30A2 | 0x30AA | 0x30A4 |
このように、JavaScriptにおける文字列は16ビットのCode Unitが順番に並んだものとして内部的に管理されています。これは、ECMAScriptの内部表現としてUTF-16を採用しているだけで、JavaScriptファイル(ソースコードを書いたファイル)のエンコーディングとは関係ありません。そのため、JavaScriptファイル自体のエンコードは、UTF-16以外の文字コードであっても問題ありません。
UTF-16を利用していることはJavaScriptの内部的な表現であるため、気にする必要がないようにも思えます。しかし、このJavaScriptがUTF-16を利用していることは、これから見ていくStringのAPIにも影響しています。このUTF-16と文字列については、次の章である「文字列とUnicode」で詳しく見ていきます。
ここでは、「JavaScriptの文字列の各要素はUTF-16のCode Unitで構成されている」ということだけを覚えておけば問題ありません。
文字列の分解と結合
文字列を配列へ分解するにはStringのsplit
メソッドを利用できます。一方、配列の要素を結合して文字列にするにはArrayのjoin
メソッドを利用できます。
この2つはよく組み合わせて利用されるため、合わせて見ていきます。
Stringのsplit
メソッドは、第一引数に指定した区切り文字で文字列を分解した配列を返します。次のコードでは、文字列を・
で区切った配列を作成しています。
const strings ="赤・青・緑".split("・");console.log(strings);// => ["赤", "青", "緑"]
分解してできた文字列の配列を結合して文字列を作る際に、Arrayのjoin
メソッドが利用できます。Arrayのjoin
メソッドの第一引数には区切り文字を指定し、その区切り文字で結合した文字列を返します。
この2つを合わせれば、区切り文字を・
から、
へ変換する処理を次のように書くことができます。・
で文字列を分割(split
)してから、区切り文字を、
にして結合(join
)すれば変換できます。
const str ="赤・青・緑".split("・").join("、");console.log(str);// => "赤、青、緑"
Stringのsplit
メソッドの第一引数には正規表現も指定できます。これを利用すると、次のように文字列をスペースで区切るような処理を簡単に書けます。/\s+/
は1つ以上のスペースにマッチする正規表現オブジェクトを作成する正規表現リテラルです。
// 文字間に1つ以上のスペースがあるconst str ="a b c d";// 1つ以上のスペースにマッチして分解するconst strings = str.split(/\s+/);console.log(strings);// => ["a", "b", "c", "d"]
文字列の長さ
Stringのlength
プロパティは文字列の要素数を返します。文字列の構成要素はCode Unitであるため、length
プロパティはCode Unitの個数を返します。
次の文字列は3つの要素(Code Unit)が並んだものであるため、length
プロパティは3
を返します。
console.log("文字列".length);// => 3
また、空文字列は要素数が0
であるため、length
プロパティの結果も0
となります。
console.log("".length);// => 0
文字列の比較
文字列の比較には===
(厳密比較演算子)を利用します。次の条件を満たしていれば同じ文字列となります。
- 文字列の要素であるCode Unitが同じ順番で並んでいるか
- 文字列の長さ(length)は同じか
難しく書いていますが、同じ文字列同士なら===
(厳密比較演算子)の結果はtrue
となります。
console.log("文字列" ==="文字列");// => true// 一致しなければfalseとなるconsole.log("JS" ==="ES");// => false// 文字列の長さが異なるのでfalseとなるconsole.log("文字列" ==="文字");// => false
また、===
などの比較演算子だけではなく、>
、<
、>=
、<=
など大小の関係演算子で文字列同士の比較もできます。
これらの関係演算子も、文字列の要素であるCode Unit同士を先頭から順番に比較します。文字列からCode Unitの数値を取得するには、StringのcharCodeAt
メソッドを利用できます。
次のコードでは、ABC
とABD
を比較した場合にどちらが大きい(Code Unitの値が大きい)かを比較しています。
// "A"と"B"のCode Unitは65と66console.log("A".charCodeAt(0));// => 65console.log("B".charCodeAt(0));// => 66// "A"(65)は"B"(66)よりCode Unitの値が小さいconsole.log("A" >"B");// => false// 先頭から順番に比較し C > D が falseであるためconsole.log("ABC" >"ABD");// => false
このように、関係演算子での文字列比較はCode Unit同士を比較しています。この結果を予測することは難しく、また直感的ではない結果が生まれることも多いです。文字の順番は国や言語によっても異なるため、国際化(Internationalization)に関する知識も必要です。
JavaScriptにおいても、ECMA-402というECMAScriptと関連する別の仕様として国際化についての取り決めがされています。この国際化に関するAPIを定義したIntlというビルトインオブジェクトもありますが、このAPIについての詳細は省略します。
文字列の一部を取得
文字列からその一部を取り出したい場合には、Stringのslice
メソッドやsubstring
メソッドが利用できます。
slice
メソッドについては、すでに配列で学んでいますが、基本的な動作は文字列でも同様です。まずはslice
メソッドについて見ていきます。
Stringのslice
メソッドは、第一引数の開始位置から第二引数の終了位置(終了位置の要素は含まない)までの範囲を取り出した新しい文字列を返します。第二引数は省略でき、省略した場合は文字列の末尾まで含んだ新しい文字列を返します。
位置にマイナスの値を指定した場合は文字列の末尾から数えた位置となります。また、第一引数の位置が第二引数の位置より大きい場合、常に空の文字列を返します。
そのため、メソッドの引数の扱い方は配列のslice
メソッドと同様です。
const str ="ABCDE";console.log(str.slice(1));// => "BCDE"console.log(str.slice(1,5));// => "BCDE"// マイナスを指定すると後ろからの位置となるconsole.log(str.slice(-1));// => "E"// インデックスが1から4の範囲を取り出すconsole.log(str.slice(1,4));// => "BCD"// 第一引数 > 第二引数の場合、常に空文字列を返すconsole.log(str.slice(4,1));// => ""
Stringのsubstring
メソッドは、slice
メソッドと同じく第一引数に開始位置、第二引数に終了位置を指定し、その範囲を取り出して新しい文字列を返します。第二引数を省略した場合の挙動も同様で、省略した場合は文字列の末尾が終了位置となります。
slice
メソッドとは異なる点として、位置にマイナスの値を指定した場合は常に0
として扱われます。また、第一引数の位置が第二引数の位置より大きい場合、第一引数と第二引数が入れ替わるという予想しにくい挙動となります。
const str ="ABCDE";console.log(str.substring(1));// => "BCDE"console.log(str.substring(1,5));// => "BCDE"// マイナスを指定すると0として扱われるconsole.log(str.substring(-1));// => "ABCDE"// 位置:1から4の範囲を取り出すconsole.log(str.substring(1,4));// => "BCD"// 第一引数 > 第二引数の場合、引数が入れ替わる// str.substring(1, 4)と同じ結果になるconsole.log(str.substring(4,1));// => "BCD"
このように、マイナスの位置や引数が交換される挙動はわかりやすいものとは言えません。そのため、slice
メソッドとsubstring
メソッドに指定する引数は、どちらとも同じ結果となる範囲に限定したほうが直感的な挙動となります。つまり、指定するインデックスは0以上にして、第二引数を指定する場合は第一引数の位置 < 第二引数の位置
にするということです。
Stringのslice
メソッドは、indexOf
メソッドなどの位置を取得するものと組み合わせて使うことが多いでしょう。次のコードでは、?
の位置をindexOf
メソッドで取得し、それ以降の文字列をslice
メソッドで切り出しています。
const url ="https://example.com?param=1";const indexOfQuery = url.indexOf("?");const queryString = url.slice(indexOfQuery);console.log(queryString);// => "?param=1"
また、配列とは異なりプリミティブ型の値である文字列は、slice
メソッドとsubstring
メソッド共に非破壊的です。機能的な違いがほとんどないため、どちらを利用するかは好みの問題となるでしょう。
文字列の検索
文字列の検索方法として、大きく分けて文字列による検索と正規表現による検索があります。
指定した文字列が文字列中に含まれているかを検索する方法として、Stringメソッドには取得したい結果ごとにメソッドが用意されています。ここでは、次の3種類の結果を取得する方法について文字列と正規表現それぞれの検索方法を見ていきます。
- マッチした箇所のインデックスを取得
- マッチした文字列の取得
- マッチしたかどうかの真偽値を取得
文字列による検索
String
オブジェクトには、指定した文字列で検索するメソッドが用意されています。
文字列によるインデックスの取得
StringのindexOf
メソッドとlastIndexOf
メソッドは、指定した文字列で検索し、その文字列が最初に現れたインデックスを返します。これらは配列のArrayのindexOf
メソッドと同じで、厳密等価演算子(===
)で文字列を検索します。一致する文字列がない場合は-1
を返します。
文字列.indexOf("検索文字列")
: 先頭から検索し、指定された文字列が最初に現れたインデックスを返す文字列.lastIndexOf("検索文字列")
: 末尾から検索し、指定された文字列が最初に現れたインデックスを返す
どちらのメソッドも一致する文字列が複数個ある場合でも、指定した検索文字列を最初に見つけた時点で検索は終了します。
// 検索対象となる文字列const str ="にわにはにわにわとりがいる";// indexOfは先頭から検索しインデックスを返す - "**にわ**にはにわにわとりがいる"// "にわ"の先頭のインデックスを返すため 0 となるconsole.log(str.indexOf("にわ"));// => 0// lastIndexOfは末尾から検索しインデックスを返す- "にわにはにわ**にわ**とりがいる"console.log(str.lastIndexOf("にわ"));// => 6// 指定した文字列が見つからない場合は -1 を返すconsole.log(str.indexOf("未知のキーワード"));// => -1
文字列にマッチした文字列の取得
文字列を検索してマッチした文字列は、検索文字列そのものになるので自明です。
次のコードでは"Script"
という文字列で検索していますが、その検索文字列にマッチする文字列はもちろん"Script"
になります。
const str ="JavaScript";const searchWord ="Script";const index = str.indexOf(searchWord);if (index !== -1) {console.log(`${searchWord}が見つかりました`);}else {console.log(`${searchWord}は見つかりませんでした`);}
真偽値の取得
「文字列」に「検索文字列」が含まれているかを検索する方法がいくつか用意されています。次の3つのメソッドはES2015で導入されました。
String.prototype.startsWith(検索文字列)
[ES2015]: 検索文字列が先頭にあるかの真偽値を返すString.prototype.endsWith(検索文字列)
[ES2015]: 検索文字列が末尾にあるかの真偽値を返すString.prototype.includes(検索文字列)
[ES2015]: 検索文字列を含むかの真偽値を返す
具体的な例をいくつか見てみましょう。
// 検索対象となる文字列const str ="にわにはにわにわとりがいる";// startsWith - 検索文字列が先頭ならtrueconsole.log(str.startsWith("にわ"));// => trueconsole.log(str.startsWith("いる"));// => false// endsWith - 検索文字列が末尾ならtrueconsole.log(str.endsWith("にわ"));// => falseconsole.log(str.endsWith("いる"));// => true// includes - 検索文字列が含まれるならtrueconsole.log(str.includes("にわ"));// => trueconsole.log(str.includes("いる"));// => true
正規表現オブジェクト
文字列による検索では、固定の文字列にマッチするものしか検索できません。一方で正規表現による検索では、あるパターン(規則性)にマッチするという柔軟な検索ができます。
正規表現は正規表現オブジェクト(RegExp
オブジェクト)として表現されます。正規表現オブジェクトはマッチする範囲を決めるパターン
と正規表現の検索モードを指定するフラグ
の2つで構成されます。正規表現のパターン内では、次の文字は特殊文字と呼ばれ、特別な意味を持ちます。特殊文字として解釈されないように入力する場合には\
(バックスラッシュ)でエスケープする必要があります。
\ ^ $ . * + ? ( ) [ ] { } |
正規表現オブジェクトを作成するには、正規表現リテラルとRegExp
コンストラクタを使う2つの方法があります。
// 正規表現リテラルで正規表現オブジェクトを作成const patternA =/パターン/フラグ;// `RegExp`コンストラクタで正規表現オブジェクトを作成const patternB =newRegExp("パターン文字列","フラグ");
正規表現リテラルは、/
と/
のリテラル内に正規表現のパターンを書くことで、正規表現オブジェクトを作成できます。次のコードでは、+
という1回以上の繰り返しを意味する特殊文字を使い、a
が1回以上連続する文字列にマッチする正規表現オブジェクトを作成しています。
const pattern =/a+/;
正規表現オブジェクトを作成するもうひとつの方法としてRegExp
コンストラクタがあります。RegExp
コンストラクタでは、文字列から正規表現オブジェクトを作成できます。
次のコードでは、RegExp
コンストラクタを使ってa
が1文字以上連続している文字列にマッチする正規表現オブジェクトを作成しています。これは先ほどの正規表現リテラルで作成した正規表現オブジェクトと同じ意味になります。
const pattern =newRegExp("a+");
正規表現リテラルとRegExp
コンストラクタの違い
正規表現リテラルとRegExp
コンストラクタの違いとして、正規表現のパターンが評価されるタイミングの違いがあります。正規表現リテラルは、ソースコードをロード(パース)した段階で正規表現のパターンが評価されます。一方で、RegExp
コンストラクタでは通常の関数と同じように、RegExp
コンストラクタを呼び出すまで正規表現のパターンは評価されません。
単独の[
という不正なパターンである正規表現を例に、評価されているタイミングの違いを見てみます。[
は対になる]
と組み合わせて利用する特殊文字であるため、正規表現のパターンに単独で書くと構文エラーの例外が発生します。
正規表現リテラルは、ソースコードのロード時に正規表現のパターンが評価されるため、次のようにmain
関数を呼び出していなくても構文エラー(SyntaxError
)が発生します。
// 正規表現リテラルはロード時にパターンが評価され、例外が発生するfunctionmain() {// `[`は対となる`]`を組み合わせる特殊文字であるため、単独で書けないconst invalidPattern =/[/;}// `main`関数を呼び出さなくても例外が発生する
一方で、RegExp
コンストラクタは実行時に正規表現のパターンが評価されるため、main
関数を呼び出すことで初めて構文エラー(SyntaxError
)が発生します。
// `RegExp`コンストラクタは実行時にパターンが評価され、例外が発生するfunctionmain() {// `[`は対となる`]`を組み合わせる特殊文字であるため、単独で書けないconst invalidPattern =newRegExp("[");}// `main`関数を呼び出すことで初めて例外が発生するmain();
これを言い換えると、正規表現リテラルはコードを書いた時点で決まったパターンの正規表現オブジェクトを作成する構文です。一方で、RegExp
コンストラクタは変数と組み合わせるなど、実行時に変わることがあるパターンの正規表現オブジェクトを作成できます。
例として、指定個数のホワイトスペース(空白文字)が連続した場合にマッチする正規表現オブジェクトで比較してみます。
次のコードでは、正規表現リテラルを使って3つ連続するホワイトスペースにマッチする正規表現オブジェクトを作成しています。\s
はスペースやタブなどのホワイトスペースにマッチする特殊文字です。また、{数字}
は指定した回数だけ繰り返しを意味する特殊文字です。
// 3つの連続するスペースなどにマッチする正規表現const pattern =/\s{3}/;
正規表現リテラルは、ロード時に正規表現のパターンが評価されるため、\s
の連続する回数を動的に変更することはできません。一方で、RegExp
コンストラクタは、実行時に正規表現のパターンが評価されるため、変数を含んだ正規表現オブジェクトを作成できます。
次のコードでは、RegExp
コンストラクタで変数spaceCount
の数だけ連続するホワイトスペースにマッチする正規表現オブジェクトを作成しています。注意点として、\
(バックスラッシュ)自体が、文字列中ではエスケープ文字であることに注意してください。そのため、RegExp
コンストラクタの引数のパターン文字列では、バックスラッシュからはじまる特殊文字は\
(バックスラッシュ)自体をエスケープする必要があります。
const spaceCount =3;// `/\s{3}/`の正規表現を文字列から作成する// "\"がエスケープ文字であるため、"\"自身を文字列として書くには、"\\"のように2つ書くconst pattern =newRegExp(`\\s{${spaceCount}}`);
このように、RegExp
コンストラクタは文字列から正規表現オブジェクトを作成できますが、特殊文字のエスケープが必要となります。そのため、正規表現リテラルで表現できる場合は、リテラルを利用したほうが簡潔でパフォーマンスもよいです。正規表現のパターンに変数を利用する場合などは、RegExp
コンストラクタを利用します。
正規表現による検索
正規表現による検索は、正規表現オブジェクトと対応したString
オブジェクトまたはRegExp
オブジェクトのメソッドを利用します。
正規表現によるインデックスの取得
StringのindexOf
メソッドの正規表現版ともいえるStringのsearch
メソッドがあります。search
メソッドは正規表現のパターンにマッチした箇所のインデックスを返し、マッチする文字列がない場合は-1
を返します。
String.prototype.indexOf(検索文字列)
: 指定された文字列にマッチした箇所のインデックスを返すString.prototype.search(/パターン/)
: 指定された正規表現のパターンにマッチした箇所のインデックスを返す
次のコードでは、数字が3つ連続しているかを検索し、該当した箇所のインデックスを返しています。\d
は、1文字の数字(0
から9
)にマッチする特殊文字です。
const str ="ABC123EFG";const searchPattern =/\d{3}/;console.log(str.search(searchPattern));// => 3
正規表現によるマッチした文字列の取得
文字列による検索では、検索した文字列そのものがマッチした文字列になります。しかし、search
メソッドの正規表現による検索は、正規表現パターンによる検索であるため、検索してマッチした文字列の長さは固定ではありません。つまり、次のようにStringのsearch
メソッドでマッチしたインデックスのみを取得しても、実際にマッチした文字列がわかりません。
const str ="abc123def";// 連続した数字にマッチする正規表現const searchPattern =/\d+/;const index = str.search(searchPattern);// => 3// `index` だけではマッチした文字列の長さがわからないstr.slice(index, index + マッチした文字列の長さ);// マッチした文字列は取得できない
そのため、マッチした文字列を取得するStringのmatch
メソッドとmatchAll
メソッドが用意されています。また、これらのメソッドは正規表現のマッチを文字列の最後まで繰り返すg
フラグ(globalの略称)によって挙動が変わります。
マッチした文字列の取得
まずは、マッチした文字列を取得するStringのmatch
メソッドから見ていきます。match
メソッドは、正規表現の/パターン/
が"文字列"
にマッチすると、マッチした文字列に関する情報を返すメソッドです。
"文字列".match(/パターン/);
match
メソッドで検索した結果、正規表現にマッチする文字列がなかった場合はnull
を返します。
console.log("文字列".match(/マッチしないパターン/));// => null
match
メソッドは正規表現のg
フラグなしのパターンで検索した場合、最初にマッチしたものが見つかった時点で検索が終了します。このときのmatch
メソッドの返り値は、index
プロパティとinput
プロパティをもった特殊な配列となります。index
プロパティにはマッチした文字列の先頭のインデックスが、input
プロパティには検索対象となった文字列全体が含まれています。
次のコードの/[a-zA-Z]+/
という正規表現はa
からZ
のどれかの文字が1つ以上連続しているものにマッチします。この正規表現にマッチした文字列は、返り値の配列からインデックスアクセスで取得できます。g
フラグなしでは、最初にマッチしたものを見つけた時点で検索が終了するので、返り値の配列には1つの要素しか含まれていません。
const str ="ABC あいう DE えお";const alphabetsPattern =/[a-zA-Z]+/;// gフラグなしでは、最初の結果のみを含んだ特殊な配列を返すconst results = str.match(alphabetsPattern);console.log(results.length);// => 1// マッチした文字列はインデックスでアクセスできるconsole.log(results[0]);// => "ABC"// マッチした文字列の先頭のインデックスconsole.log(results.index);// => 0// 検索対象となった文字列全体console.log(results.input);// => "ABC あいう DE えお"
match
メソッドは正規表現のg
フラグありのパターンで検索した場合、マッチしたすべての文字列を含んだ配列を返します。
次のコードの/[a-zA-Z]+/g
という正規表現はa
からZ
のどれかの文字が1つ以上連続しているものに繰り返しマッチします。この正規表現にマッチする箇所は"ABC"と"DE"の2つとなるため、match
メソッドの返り値である配列にも2つの要素が含まれています。
const str ="ABC あいう DE えお";const alphabetsPattern =/[a-zA-Z]+/g;// gフラグありでは、すべての検索結果を含む配列を返すconst resultsWithG = str.match(alphabetsPattern);console.log(resultsWithG.length);// => 2console.log(resultsWithG[0]);// => "ABC"console.log(resultsWithG[1]);// => "DE"// indexとinputはgフラグありの場合は追加されないconsole.log(resultsWithG.index);// => undefinedconsole.log(resultsWithG.input);// => undefined
このときのmatch
メソッドの返り値である配列にはindex
とinput
プロパティはありません。なぜなら、複数の箇所にマッチする場合においては、1つのindex
プロパティでは意味が一意に決まらないためです。
Stringのmatch
メソッドの挙動をまとめると次のようになります。
- マッチしない場合は、
null
を返す - マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
- 正規表現の
g
フラグがある場合は、マッチしたすべての結果を含んだただの配列を返す
ES2020では、正規表現のg
フラグを使った繰り返しマッチする場合においても、それぞれマッチした文字列ごとの情報を得るためのStringのmatchAll
が追加されています。matchAll
メソッドは、マッチした結果をIteratorで返します。
次のコードでは、matchAll
メソッドでアルファベットにマッチする結果のIteratorオブジェクトを取得しています。Iteratorオブジェクトはfor...of
構文で反復処理すると、Iteratorから値を1つずつ取り出して処理できます(詳細は「ループと反復処理」の章を参照)。このときの反復処理で取得できる値は、それぞれのマッチした文字列とindex
とinput
プロパティを持つ特殊な配列となります。
const str ="ABC あいう DE えお";const alphabetsPattern =/[a-zA-Z]+/g;// matchAllはIteratorを返すconst matchesIterator = str.matchAll(alphabetsPattern);for (const matchof matchesIterator) {// マッチした要素ごとの情報を含んでいるconsole.log(`match: "${match[0]}", index:${match.index}, input: "${match.input}"`);}// 次の順番でコンソールに出力される// match: "ABC", index: 0, input: "ABC あいう DE えお"// match: "DE", index: 8, input: "ABC あいう DE えお"
そのため、正規表現のg
フラグを使った繰り返しマッチを行う場合には、match
メソッドではなくmatchAll
メソッドを利用します。また、matchAll
メソッドはg
フラグなしの正規表現はサポートしていないため、g
フラグなしの正規表現を渡した場合は例外が発生します。
マッチした文字列の一部を取得
Stringのmatch
メソッドとmatchAll
メソッドは、どちらも正規表現のキャプチャリングに対応しています。キャプチャリングとは、正規表現中で/パターン1(パターン2)/
のようにカッコで囲んだ部分を取り出すことです。このキャプチャリングによって、正規表現でマッチした一部分だけを取り出せます。
match
メソッドとmatchAll
メソッドはどちらもマッチした結果を配列として返します。
そのマッチしているパターンにキャプチャが含まれている場合は、返り値の配列へキャプチャした部分が追加されていきます。配列の先頭にはマッチした文字列全体が入り、順番にキャプチャリング((
と)
)で囲んだ範囲が配列に含まれます。
const [マッチした全体の文字列, キャプチャ1, キャプチャ2] = 文字列.match(/パターン(キャプチャ1)と(キャプチャ2)/);
次のコードでは、ECMAScript 数字
の数字
部分だけを取り出そうとしています。Stringのmatch
メソッドとキャプチャリングによって数字(\d+
)にマッチする部分を取り出しています。
// "ECMAScript (数字+)"にマッチするが、欲しい文字列は数字の部分のみconst pattern =/ECMAScript (\d+)/;// 返り値は0番目がマッチした全体、1番目がキャプチャの1番目というように対応している// [マッチした全部の文字列, キャプチャの1番目, キャプチャの2番目 ....]const [all, capture1] ="ECMAScript 6".match(pattern);console.log(all);// => "ECMAScript 6"console.log(capture1);// => "6"
正規表現のg
フラグを使い繰り返し文字列にマッチする場合には、matchAll
メソッドを利用します。先ほども紹介したように、match
メソッドは繰り返しマッチした場合に、それぞれ個別のマッチした情報を取得できないためです。
次のコードでは、ES数字
の数字(\d+
)にマッチする部分を取り出しています。matchAll
の返り値であるIteratorを反復処理することで、それぞれマッチしたキャプチャを取り出しています。
// "ES(数字+)"にマッチするが、欲しい文字列は数字の部分のみconst pattern =/ES(\d+)/g;// iteratorを返すconst matchesIterator ="ES2015、ES2016、ES2017".matchAll(pattern);for (const matchof matchesIterator) {// マッチした要素ごとの情報を含んでいる// 0番目はマッチした文字列全体、1番目がキャプチャの1番目である数字console.log(`match: "${match[0]}", capture1:${match[1]}, index:${match.index}, input: "${match.input}"`);}// 次の順番でコンソールに出力される// match: "ES2015", capture1: 2015, index: 0, input: "ES2015、ES2016、ES2017"// match: "ES2016", capture1: 2016, index: 7, input: "ES2015、ES2016、ES2017"// match: "ES2017", capture1: 2017, index: 14, input: "ES2015、ES2016、ES2017"
[コラム] RegExp.prototype.execでのString.prototype.matchAll
StringのmatchAll
メソッドは、ES2020で導入されたメソッドです。それまでは、RegExpのexec
メソッドというStringのmatch
メソッドによく似た挙動をするメソッドを利用して、StringのmatchAll
メソッド相当の表現を実装していました。
RegExpのexec
メソッドは、引数に文字列を受け取るメソッドです。
/pattern/.exec("文字列");
RegExpのexec
メソッドはg
フラグなしのパターンで検索した場合、マッチした最初の結果のみを含む特殊な配列を返します。このときのexec
メソッドの返り値である配列がindex
プロパティとinput
プロパティが追加された特殊な配列となるのは、Stringのmatch
メソッドと同様です。
const str ="ABC あいう DE えお";const alphabetsPattern =/[a-zA-Z]+/;// gフラグなしでは、最初の結果のみを持つ配列を返すconst results = alphabetsPattern.exec(str);console.log(results.length);// => 1console.log(results[0]);// => "ABC"// マッチした文字列の先頭のインデックスconsole.log(results.index);// => 0// 検索対象となった文字列全体console.log(results.input);// => "ABC あいう DE えお"
RegExpのexec
メソッドはg
フラグありのパターンで検索した場合も、マッチした最初の結果のみを含む特殊な配列を返します。この点はStringのmatch
メソッドとは異なります。また、最後にマッチした文字列末尾のインデックスを正規表現オブジェクトのlastIndex
プロパティに記録します。そしてもう一度exec
メソッドを呼び出すと最後にマッチした末尾のインデックス(lastIndex
プロパティの位置)から検索が開始されます。
const str ="ABC あいう DE えお";const alphabetsPattern =/[a-zA-Z]+/g;// まだ一度も検索していないので、lastIndexは0となり先頭から検索が開始されるconsole.log(alphabetsPattern.lastIndex);// => 0// gフラグありでも、一回目の結果は同じだが、`lastIndex`プロパティが更新されるconst result1 = alphabetsPattern.exec(str);console.log(result1[0]);// => "ABC"console.log(alphabetsPattern.lastIndex);// => 3// 2回目の検索が、`lastIndex`の値のインデックスから開始されるconst result2 = alphabetsPattern.exec(str);console.log(result2[0]);// => "DE"console.log(alphabetsPattern.lastIndex);// => 10// 検索結果が見つからない場合はnullを返し、`lastIndex`プロパティは0にリセットされるconst result3 = alphabetsPattern.exec(str);console.log(result3);// => nullconsole.log(alphabetsPattern.lastIndex);// => 0
RegExpのexec
メソッドの挙動をまとめると次のようになります。正規表現のg
フラグがない場合は、Stringのmatch
メソッドと同じ結果です。一方で、正規表現のg
フラグがある場合は、Stringのmatch
メソッドとは異なる挙動をします。
- マッチしない場合は、
null
を返す - マッチした場合は、マッチした文字列を含んだ特殊な配列を返す
- 正規表現の
g
フラグがある場合は、マッチした文字列を含んだ特殊な配列を返し、マッチした末尾のインデックスを正規表現オブジェクトのlastIndex
プロパティに記録する
この正規表現のg
フラグとexec
メソッドで検索した場合に、lastIndex
プロパティが検索ごとに更新される仕組みを利用して、マッチするすべての結果を取得できます。
次のコードでは、RegExpのexec
メソッドを使い、アルファベットにマッチした結果をmatches
に保持しています。g
フラグがある場合のexec
メソッドでは最後にマッチした位置が記録されているため、while
文で反復処理して続きから検索しています。また、exec
メソッドはマッチしなければnull
を返すため、マッチするものがなくなればwhile文から自動的に脱出します。
const str ="ABC あいう DE えお";const alphabetsPattern =/[a-zA-Z]+/g;let matches;while (matches = alphabetsPattern.exec(str)) {// RegExpの`exec`メソッドの返り値は`index`プロパティなどを含む特殊な配列console.log(`match:${matches[0]}, index:${matches.index}, lastIndex:${alphabetsPattern.lastIndex}`);}// 次の順番でコンソールに出力される// match: ABC, index: 0, lastIndex: 3// match: DE, index: 8, lastIndex: 10
このようにRegExpのexec
メソッドと正規表現のg
フラグを使い、StringのmatchAll
メソッド相当の反復処理を実装していました。RegExpのexec
はIteratorオブジェクトという反復処理のためのオブジェクトが導入される以前からあるメソッドです。
StringのmatchAll
がIteratorを扱うわかりやすい反復処理に比べて、RegExpのexec
メソッドはwhile
文などで手動で反復処理を書く必要があるため直感的ではありません。そのため、StringのmatchAll
メソッドが利用できる場合に、RegExpのexec
メソッドを利用する必要はありません。
真偽値を取得
正規表現オブジェクトを使って、そのパターンにマッチするかをテストするには、RegExpのtest
メソッドを利用できます。
正規表現のパターンには、パターンの位置を指定する特殊文字があります。そのため、「文字列による検索」で登場したメソッドは、特殊文字とRegExpのtest
メソッドで表現できます。
- Stringの
startsWith
相当:/^パターン/.test(文字列)
^
は先頭に一致する特殊文字
- Stringの
endsWith
相当:/パターン$/.test(文字列)
$
は末尾に一致する特殊文字
- Stringの
includes
相当:/パターン/.test(文字列)
具体的な例を見てみましょう。
// 検索対象となる文字列const str ="にわにはにわにわとりがいる";// ^ - 検索文字列が先頭ならtrueconsole.log(/^にわ/.test(str));// => trueconsole.log(/^いる/.test(str));// => false// $ - 検索文字列が末尾ならtrueconsole.log(/にわ$/.test(str));// => falseconsole.log(/いる$/.test(str));// => true// 検索文字列が含まれるならtrueconsole.log(/にわ/.test(str));// => trueconsole.log(/いる/.test(str));// => true
そのほかにも、正規表現では繰り返しや文字の集合などを特殊文字で表現できるため柔軟な検索が可能です。
文字列と正規表現どちらを使うべきか
Stringメソッドでの検索と同等のことは、正規表現でもできることがわかりました。Stringメソッドと正規表現で同じ結果が得られる場合はどちらを利用するのがよいでしょうか?
正規表現は曖昧な検索に強く、特殊文字を使うことで柔軟な検索結果を得られます。一方、曖昧であるため、コードを見ても何を検索しているかが正規表現のパターン自体からわからないことがあります。
次の例は、/
からはじまり/
で終わる文字列かを判定しようとしています。この判定を正規表現とStringメソッドを使ってそれぞれ実装しています(これは意図的に正規表現に不利な例となっています)。
正規表現の場合、/^\/.*\/$/
のようにパターンそのものを見ても何をしたいのかはひと目ではわかりにくいです。Stringメソッドの場合は、/
からはじまり/
で終わるかを判定してることがそのままコードに表現できています。
const str ="/正規表現のような文字列/";// 正規表現で`/`からはじまり`/`で終わる文字列のパターンconst regExpLikePattern =/^\/.*\/$/;// RegExpの`test`メソッドでパターンにマッチするかを判定console.log(regExpLikePattern.test(str));// => true// Stringメソッドで、`/`からはじまり`/`で終わる文字列かを判定する関数constisRegExpLikeString = (str) => {return str.startsWith("/") && str.endsWith("/");};console.log(isRegExpLikeString(str));// => true
このように、正規表現は柔軟で便利ですが、コード上から意図が消えてしまいやすいです。そのため、正規表現を扱う際にはコメントや変数名で具体的な意図を補足したほうがよいでしょう。
「Stringメソッドと正規表現で同じ結果が得られる場合はどちらを利用するのがよいでしょうか?」という疑問に戻ります。Stringメソッドで表現できることはStringメソッドで表現し、柔軟性や曖昧な検索が必要な場合はコメントとともに正規表現を利用するという方針を推奨します。
正規表現についてより詳しくはMDNの正規表現ドキュメントや、コンソールで実行しながら試せるregex101のようなサイトを参照してください。
文字列の置換/削除
文字列の一部を置換したり削除するにはStringのreplace
メソッドを利用します。「データ型とリテラル」で説明したようにプリミティブ型である文字列は不変な特性を持ちます。そのため、文字列から一部の文字を削除するような操作はできません。
つまり、delete
演算子は文字列に対して利用できません。strict modeでは、delete
演算子で削除できないプロパティを削除しようとするとエラーが発生します。strict modeでない場合は、エラーも発生せず単に無視されます(詳細は「JavaScriptとは」のstrict modeを参照)。
"use strict";const str ="文字列";// 文字列の0番目の削除を試みるがStrict modeでは例外が発生するdelete str[0];// => TypeError: property 0 is non-configurable and can't be deleted
代わりに、Stringのreplace
メソッドで、削除したい文字を取り除いた新しい文字列を返すことで削除を表現します。replace
メソッドは、文字列から第一引数の検索文字列
または正規表現にマッチする部分を、第二引数の置換文字列
へ置換します。第一引数には、文字列と正規表現を指定できます。
文字列.replace("検索文字列","置換文字列");文字列.replace(/パターン/,"置換文字列");
次のように、replace
メソッドで削除したい部分を空文字列へ置換することで、文字列を削除できます。
const str ="文字列";// "文字"を""(空文字列)へ置換することで"削除"を表現const newStr = str.replace("文字","");console.log(newStr);// => "列"
replace
メソッドには正規表現も指定できます。g
フラグを有効化した正規表現を渡すことで、文字列からパターンにマッチするものをすべて置換できます。
// 検索対象となる文字列const str ="にわにはにわにわとりがいる";// 文字列を指定した場合は、最初に一致したものだけが置換されるconsole.log(str.replace("にわ","niwa"));// => "niwaにはにわにわとりがいる"// `g`フラグなし正規表現の場合は、最初に一致したものだけが置換されるconsole.log(str.replace(/にわ/,"niwa"));// => "niwaにはにわにわとりがいる"// `g`フラグあり正規表現の場合は、繰り返し置換を行うconsole.log(str.replace(/にわ/g,"niwa"));// => "niwaにはniwaniwaとりがいる"
文字列から検索文字列にマッチするものをすべて置換する場合には、ES2021で追加されたStringのreplaceAll
メソッドも利用できます。replace
メソッドでは、最初に一致したものだけが置換されますが、replaceAll
メソッドでは一致したものがすべて置換されます。
Stringのreplace
とg
フラグ付きの正規表現を使った場合との違いとして、StringのreplaceAll
メソッドでは、正規表現ではなく文字列を使ってすべてを置換できます。そのため、正規表現では特殊な意味を持つ?
のような文字列も検索文字列にそのまま書いて置換ができます。
// 検索対象となる文字列const str ="???";// replaceメソッドに文字列を指定した場合は、最初に一致したものだけが置換されるconsole.log(str.replace("?","!"));// => "!??"// replaceAllメソッドに文字列を指定した場合は、一致したものがすべて置換されるconsole.log(str.replaceAll("?","!"));// => "!!!"// replaceメソッドの場合は、正規表現の特殊文字はエスケープが必要となるconsole.log(str.replace(/\?/g,"!"));// => "!!!"// replaceAllメソッドにも正規表現を渡せるが、この場合はエスケープが必要となるためreplaceと同じconsole.log(str.replaceAll(/\?/g,"!"));// => "!!!"
replace
メソッドとreplaceAll
メソッドでは、キャプチャした文字列を利用して複雑な置換処理もできます。
replace
メソッドとreplaceAll
メソッドの第二引数にはコールバック関数を渡せます。第一引数のパターン
にマッチした部分がコールバック関数の返り値で置換されます。コールバック関数の第一引数にはパターン
に一致した文字列全体、第二引数以降へキャプチャした文字列が順番に入ります。
const 置換した結果の文字列 = 文字列.replace(/(パターン)/,(all, ...captures) => {return 置換したい文字列;});
例として、2017-03-01
を2017年03月01日
に置換する処理を書いてみましょう。
/(\d{4})-(\d{2})-(\d{2})/g
という正規表現が"2017-03-01"
という文字列にマッチします。コールバック関数のyear
、month
、day
にはそれぞれキャプチャした文字列が入り、マッチした文字列全体がコールバック関数の返り値に置換されます。
functiontoDateJa(dateString) {// パターンにマッチしたときのみ、コールバック関数で置換処理が行われるreturn dateString.replace(/(\d{4})-(\d{2})-(\d{2})/g,(all, year, month, day) => {// `all`には、マッチした文字列全体が入っているが今回は利用しない// `all`が次の返す値で置換されるイメージreturn`${year}年${month}月${day}日`; });}// マッチしない文字列の場合は、そのままの文字列が返るconsole.log(toDateJa("本日ハ晴天ナリ"));// => "本日ハ晴天ナリ"// マッチした場合は置換した結果を返すconsole.log(toDateJa("今日は2017-03-01です"));// => "今日は2017年03月01日です"
文字列の組み立て
最後に文字列の組み立てについて見ていきましょう。最初に述べたようにこの章の目的は、「自由に文字列を作れるようになること」です。
文字列を単純に結合したり置換することで新しい文字列を作れることがわかりました。一方、構造的な文字列の場合は単純に結合するだけでは意味が異なってしまうことがあります。
ここでの構造的な文字列とは、URL文字列やファイルパス文字列といった文字列中にコンテキストを持っているものを指します。たとえば、URL文字列は次のような構造を持っており、それぞれの要素に入る文字列の種類などが決められています(詳細は「URL Standard」を参照)。
"https://example.com/index.html" ^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^ | | | scheme host pathname
これらの文字列を作成する場合は、文字列結合演算子(+
)で単純に結合するよりも専用の関数を用意するほうが安全です。
たとえば、次のようにbaseURL
とpathname
を渡し、それらを結合したURLにあるリソースを取得するgetResource
関数があるとします。このgetResource
関数には、ベースURL(baseURL
)とパス(pathname
)を引数に渡して利用します。
// `baseURL`と`pathname`にあるリソースを取得するfunctiongetResource(baseURL, pathname) {const url = baseURL + pathname;console.log(url);// => "http://example.com/resouces/example.js"// 省略) リソースを取得する処理...}const baseURL ="http://example.com/resouces";const pathname ="/example.js";getResource(baseURL, pathname);
しかし、人によっては、baseURL
の末尾には/
が含まれると考える場合もあります。getResource
関数は、baseURL
の末尾に/
が含まれているケースを想定していませんでした。そのため、意図しないURLからリソースを取得するという問題が発生します。
// `baseURL`と`pathname`にあるリソースを取得するfunctiongetResource(baseURL, pathname) {const url = baseURL + pathname;// `/` と `/` が2つ重なってしまっているconsole.log(url);// => "http://example.com/resouces//example.js"// 省略) リソースを取得する処理...}const baseURL ="http://example.com/resouces/";const pathname ="/example.js";getResource(baseURL, pathname);
この問題が難しいところは、結合してできたurl
は文字列としては正しいため、エラーではないということです。つまり、一見すると問題ないように見えますが、実際に動かしてみて初めてわかるような問題が生じやすいのです。
そのため、このような構造的な文字列を扱う場合は、専用の関数や専用のオブジェクトを作ることで安全に文字列を処理します。
先ほどのような、URL文字列の結合を安全に行うには、入力されるbaseURL
文字列の表記揺れを吸収する仕組みを作成します。次のbaseJoin
関数はベースURLとパスを結合した文字列を返しますが、ベースURLの末尾に/
があるかの揺れを吸収しています。
// ベースURLとパスを結合した文字列を返すfunctionbaseJoin(baseURL, pathname) {// 末尾に / がある場合は、/ を削除してから結合するconst stripSlashBaseURL = baseURL.replace(/\/$/,"");return stripSlashBaseURL + pathname;}// `baseURL`と`pathname`にあるリソースを取得するfunctiongetResource(baseURL, pathname) {const url =baseJoin(baseURL, pathname);// baseURLの末尾に / があってもなくても同じ結果となるconsole.log(url);// => "http://example.com/resouces/example.js"// 省略) リソースを取得する処理...}const baseURL ="http://example.com/resouces/";const pathname ="/example.js";getResource(baseURL, pathname);
ECMAScriptの範囲ではありませんが、URLやファイルパスといった典型的なものに対してはすでに専用のものがあります。URLを扱うものとしてウェブ標準APIであるURLオブジェクト、ファイルパスを扱うものとしてはNode.jsのコアモジュールであるPathモジュールなどがあります。専用の仕組みがある場合は、直接+
演算子で結合するような文字列処理は避けるべきです。
[ES2015] タグつきテンプレート関数
JavaScriptでは、テンプレートとなる文字列に対して一部分だけを変更する処理を行う方法として、タグつきテンプレート関数があります。タグつきテンプレート関数とは、関数`テンプレート`
という形式で記述する関数とテンプレートリテラルを合わせた表現です。関数の呼び出しに関数(`テンプレート`)
ではなく、関数`テンプレート`
という書式を使っていることに注意してください。
通常の関数として呼び出した場合、関数の引数にはただの文字列が渡ってきます。
functiontag(str) {// 引数`str`にはただの文字列が渡ってくるconsole.log(str);// => "template 0 literal 1"}// ()をつけて関数を呼び出すtag(`template${0} literal${1}`);
しかし、()
ではなく関数`テンプレート`
と記述することで、関数
が受け取る引数にはタグつきテンプレート向けの値が渡ってきます。このとき、関数の第一引数にはテンプレートの中身が${}
で区切られた文字列の配列、第二引数以降は${}
の中に書いた式の評価結果が順番に渡されます。
// 呼び出し方によって受け取る引数の形式が変わるfunctiontag(strings, ...values) {// stringsは文字列のパーツが${}で区切られた配列となるconsole.log(strings);// => ["template "," literal ",""]// valuesには${}の評価値が順番に入るconsole.log(values);// => [0, 1]}// ()をつけずにテンプレートを呼び出すtag`template${0} literal${1}`;
どちらも同じ関数ですが、関数`テンプレート`
という書式で呼び出すと渡される引数が特殊な形になります。そのため、タグつきテンプレートで利用する関数のことをタグ関数(Tag function)と呼び分けることにします。
まずは引数をどう扱うかを見ていくために、タグつきテンプレートの内容をそのまま結合して返すstringRaw
というタグ関数を実装してみます。Arrayのreduce
メソッドを使うことで、テンプレートの文字列と変数を順番に結合できます(reduce
メソッドについては「配列」の章を参照)。
// テンプレートを順番どおりに結合した文字列を返すタグ関数functionstringRaw(strings, ...values) {// 配列から文字列を返すためにreduceメソッドを利用する// resultの初期値はstrings[0]の値となるreturn strings.reduce((result, str, i) => {console.log([result, values[i -1], str]);// それぞれループで次のような出力となる// 1度目: ["template ", 0, " literal "]// 2度目: ["template 0 literal ", 1, ""]return result + values[i -1] + str; });}// 関数`テンプレートリテラル` という形で呼び出すconsole.log(stringRaw`template${0} literal${1}`);// => "template 0 literal 1"
ここで実装したstringRaw
タグ関数と同様のものが、String.raw
メソッド[ES2015]として提供されています。
console.log(String.raw`template${0} literal${1}`);// => "template 0 literal 1"
タグつきテンプレート関数を利用することで、テンプレートとなる文字列に対して特定の形式に変換したデータを埋め込むといったテンプレート処理が行えます。
次のコードでは、テンプレート中の変数をURLエスケープしてから埋め込むタグつきテンプレート関数を定義しています。encodeURIComponent
関数は引数の値をURLエスケープする関数です。escapeURL
では受け取った変数をencodeURIComponent
関数でURLエスケープしてから埋め込んでいます。
// 変数をURLエスケープするタグ関数functionescapeURL(strings, ...values) {return strings.reduce((result, str, i) => {return result +encodeURIComponent(values[i -1]) + str; });}const input ="A&B";// escapeURLタグ関数を使ったタグつきテンプレートconst escapedURL = escapeURL`https://example.com/search?q=${input}&sort=desc`;console.log(escapedURL);// => "https://example.com/search?q=A%26B&sort=desc"
このようにタグつきテンプレートリテラルを使うことで、コンテキストに応じた処理をつけ加えることができます。この機能はJavaScript内にHTMLなどの別の言語やDSL(ドメイン固有言語)を埋め込む際に利用されることが多いです。
終わりに
この章では、JavaScriptにおける文字列(String
オブジェクト)について紹介しました。文字列処理するStringメソッドにはさまざまなものがあり、正規表現と組み合わせて使うものも含まれます。
正規表現は、正規表現のみで1冊の本が作れるようなJavaScript言語内にある別言語です。詳細はMDNの正規表現ドキュメントなども参照してください。
文字列は一見すると単純に見えますが、URLやパスといったコンテキストを持つものもあります。それらの文字列を安全に扱うためには、コンテキストに応じた処理が必要になります。また、タグつきテンプレートリテラルを利用することで、テンプレート中の変数を自動でエスケープするといった処理を実現できます。
1. Unicodeのカタカナの一覧https://unicode-table.com/jp/#katakana から取り出したテーブルです。 ↩