配列
配列はJavaScriptの中でもよく使われるオブジェクトです。
配列とは値に順序をつけて格納できるオブジェクトです。配列に格納したそれぞれの値のことを要素、それぞれの要素の位置のことをインデックス(index
)と呼びます。インデックスは先頭の要素から0
、1
、2
のように0
からはじまる連番となります。
またJavaScriptにおける配列は可変長です。そのため配列を作成後に配列へ要素を追加したり、配列から要素を削除できます。
この章では、配列の基本的な操作と配列を扱う場合においてのパターンについて学びます。
配列の作成とアクセス
配列の作成と要素へのアクセス方法は「データ型とリテラル」の章ですでに紹介していますが、もう一度振り返ってみましょう。
配列の作成には配列リテラルを使います。配列リテラル([
と]
)の中に要素をカンマ(,
)区切りで記述するだけです。
const emptyArray = [];const numbers = [1,2,3];// 2次元配列(配列の配列)const matrix = [ ["a","b"], ["c","d"]];
作成した配列の要素のインデックスとなる数値を、配列[インデックス]
と記述することで、そのインデックスの要素を配列から読み取れます。配列の先頭要素のインデックスは0
となります。配列のインデックスは、0
以上2^32 - 1
未満の整数となります。
const array = ["one","two","three"];console.log(array[0]);// => "one"
2次元配列(配列の配列)からの値の読み取りも同様に配列[インデックス]
でアクセスできます。配列[0][0]
は、配列の0
番目の要素である配列(["a", "b"]
)の0
番目の要素を読み取ります。
// 2次元配列(配列の配列)const matrix = [ ["a","b"], ["c","d"]];console.log(matrix[0][0]);// => "a"
配列のlength
プロパティは配列の要素の数を返します。そのため、配列の最後の要素へアクセスするにはarray.length - 1
をインデックスとして利用できます。
const array = ["one","two","three"];console.log(array.length);// => 3// 配列の要素数 - 1 が 最後の要素のインデックスとなるconsole.log(array[array.length -1]);// => "three"
一方、存在しないインデックスにアクセスした場合はどうなるでしょうか? JavaScriptでは、存在しないインデックスに対してアクセスした場合に、例外ではなくundefined
を返します。
const array = ["one","two","three"];// `array`にはインデックスが100の要素は定義されていないconsole.log(array[100]);// => undefined
これは、配列がオブジェクトであることを考えると、次のように存在しないプロパティへアクセスしているのと原理は同じです。オブジェクトでも、存在しないプロパティへアクセスした場合にはundefined
が返ってきます。
const obj = {"0":"one","1":"two","2":"three","length":3};// obj["100"]は定義されていないため、undefinedが返るconsole.log(obj[100]);// => undefined
また、配列は常にlength
の数だけ要素を持っているとは限りません。次のように、配列リテラルでは値を省略することで、未定義の要素を含めることができます。このような、配列の中に隙間があるものを疎な配列と呼びます。一方、隙間がなくすべてのインデックスに要素がある配列を密な配列と呼びます。
// 未定義の箇所が1つ含まれる疎な配列// インデックスが1の値を省略しているので、カンマが2つ続いていることに注意const sparseArray = [1, ,3];console.log(sparseArray.length);// => 3// 1番目の要素は存在しないため undefined が返るconsole.log(sparseArray[1]);// => undefined
[ES2022]Array.prototype.at
配列の要素にアクセスするには配列[インデックス]
という構文を使うことを紹介しました。その際に、配列の末尾の要素へアクセスするには、array[array.length - 1]
というlength
プロパティを使う必要があります。array
を2回書く必要があるなど、末尾の要素へのアクセスは少し手間が必要になっていました。
この問題を解決するためES2022では、相対的なインデックスの値を指定して配列の要素へアクセスできるArray.prototype.at
メソッドが追加されました。Arrayのat
メソッドは、配列[インデックス]
とよく似ていますが、引数には相対的なインデックスの値を引数として渡せます。.at(0)
や.at(1)
などのように0以上のインデックスを渡した場合は、配列[インデックス]
と同じく指定した位置の要素へアクセスできます。一方で、.at(-1)
のようにマイナスのインデックスを渡した場合は、末尾から数えた位置の要素へアクセスできます。
const array = ["a","b","c"];//console.log(array.at(0));// => "a"console.log(array.at(1));// => "b"// 後ろから1つ目の要素にアクセスconsole.log(array.at(-1));// => "c"// -1は、次のように書いた場合と同じ結果console.log(array[array.length -1]);// => "c"
配列[インデックス]
のインデックスに-1
を指定すると、配列オブジェクトの"-1"
というプロパティ名へのアクセスとなります。そのため配列[-1]
と書くと、大抵の場合はundefined
が返されます。
const array = ["a","b","c"];console.log(array[-1]);// => undefined
オブジェクトが配列かどうかを判定する
あるオブジェクトが配列かどうかを判定するにはArray.isArray
静的メソッドを利用します。Array.isArray
静的メソッドは引数が配列ならばtrue
を返します。
const obj = {};const array = [];console.log(Array.isArray(obj));// => falseconsole.log(Array.isArray(array));// => true
また、typeof
演算子では配列かどうかを判定することはできません。配列もオブジェクトの一種であるため、typeof
演算子の結果が"object"
となるためです。
const array = [];console.log(typeof array);// => "object"
[コラム] [ES2015] TypedArray
JavaScriptの配列は可変長のみですが、TypedArray
という固定長でかつ型つきの配列を扱う別のオブジェクトが存在します。TypedArray
はバイナリデータのバッファを示すために使われるデータ型で、WebGLやバイナリを扱う場面で利用されます。文字列や数値などのプリミティブ型の値を直接は利用できないため、通常の配列とは用途や使い勝手が異なります。
また、TypedArrayはArray.isArray
のメソッドの結果がfalse
となることからも別物と考えてよいでしょう。
// TypedArrayを作成const typedArray =newInt8Array(8);console.log(Array.isArray(typedArray));// => false
そのため、JavaScriptで配列といった場合にはArray
を示します。
[ES2015] 配列と分割代入
配列の指定したインデックスの値を変数として定義し直す場合には、分割代入(Destructuring assignment)が利用できます。
配列の分割代入では、左辺に配列リテラルのような構文で定義したい変数名を書きます。右辺の配列から対応するインデックスの要素が、左辺で定義した変数に代入されます。
次のコードでは、左辺に定義した変数に対して、右辺の配列から対応するインデックスの要素が代入されます。first
にはインデックスが0
の要素、second
にはインデックスが1
の要素、third
にはインデックスが2
の要素が代入されます。
const array = ["one","two","three"];const [first, second, third] = array;console.log(first);// => "one"console.log(second);// => "two"console.log(third);// => "three"
[コラム] undefinedの要素と未定義の要素の違い
疎な配列で該当するインデックスに要素がない場合はundefined
を返します。しかし、undefined
という値も存在するため、配列にundefined
という値がある場合に区別できません。
次のコードでは、undefined
という値を要素として定義した密な配列と、要素そのものがない疎な配列を定義しています。どちらも要素にアクセスした結果はundefined
となり、区別できていないことがわかります。
// 要素として`undefined`を持つ密な配列const denseArray = [1,undefined,3];// 要素そのものがない疎な配列const sparseArray = [1, ,3];console.log(denseArray[1]);// => undefinedconsole.log(sparseArray[1]);// => undefined
この違いを見つける方法として利用できるのが、Object.hasOwn
静的メソッドです。Object.hasOwn
静的メソッドを使うことで、配列オブジェクトに対して指定したインデックスに要素自体が存在するかを判定できます。
const denseArray = [1,undefined,3];const sparseArray = [1, ,3];// 要素自体は存在し、その値が`undefined`console.log(Object.hasOwn(denseArray,1));// => true// 要素自体が存在しないconsole.log(Object.hasOwn(sparseArray,1));// => false
配列から要素を検索
配列から指定した要素を検索する目的には、主に次の3つがあります。
- その要素のインデックスが欲しい場合
- その要素自体が欲しい場合
- その要素が含まれているかという真偽値が欲しい場合
配列にはそれぞれに対応したメソッドが用意されているため、目的別に見ていきます。
インデックスを取得
指定した要素が配列のどの位置にあるかを知りたい場合、ArrayのindexOf
メソッドやfindIndex
メソッド[ES2015]を利用します。要素の位置のことをインデックス(index
)と呼ぶため、メソッド名にもindex
という名前が入っています。
次のコードでは、ArrayのindexOf
メソッドを利用して、配列の中から"JavaScript"
という文字列のインデックスを取得しています。indexOf
メソッドは引数と厳密等価演算子(===
)で一致する要素を先頭から検索して該当する要素のインデックスを返し、該当する要素がない場合は-1
を返します。indexOf
メソッドには対となるlastIndexOf
メソッドがあり、lastIndexOf
メソッドでは末尾から検索した結果が得られます。
const array = ["Java","JavaScript","Ruby","JavaScript"];// 先頭から探索して最初に見つかった"JavaScript"のインデックスを取得const indexOfJS = array.indexOf("JavaScript");console.log(indexOfJS);// => 1// 末尾から探索して最初に見つかった"JavaScript"のインデックスを取得const lastIndexOfJS = array.lastIndexOf("JavaScript");console.log(lastIndexOfJS);// => 3console.log(array[indexOfJS]);// => "JavaScript"console.log(array[lastIndexOfJS]);// => "JavaScript"// "JS" という要素はないため `-1` が返されるconsole.log(array.indexOf("JS"));// => -1console.log(array.lastIndexOf("JS"));// => -1
indexOf
メソッドは配列からプリミティブな要素を発見できますが、オブジェクトは持っているプロパティが同じでも別オブジェクトだと異なるものとして扱われます。次のコードを見ると、同じプロパティを持つ異なるオブジェクトは、indexOf
メソッドでは見つけることができません。これは、異なる参照を持つオブジェクト同士は===
で比較しても一致しないためです。
const obj = {key:"value" };const array = ["A","B", obj];console.log(array.indexOf({key:"value" }));// => -1// リテラルは新しいオブジェクトを作るため、異なるオブジェクトだと判定されるconsole.log(obj === {key:"value" });// => false// 等価のオブジェクトを検索してインデックスを返すconsole.log(array.indexOf(obj));// => 2
このように、異なるオブジェクトだが値は同じものを見つけたい場合には、ArrayのfindIndex
メソッドが利用できます。findIndex
メソッドの引数には配列の各要素をテストする関数をコールバック関数として渡します。indexOf
メソッドとは異なり、テストする処理を自由に書けます。これにより、プロパティの値が同じ要素を配列から見つけて、その要素のインデックスを得ることができます。
// colorプロパティを持つオブジェクトの配列const colors = [ {"color":"red" }, {"color":"green" }, {"color":"blue" }];// `color`プロパティが"blue"のオブジェクトのインデックスを取得const indexOfBlue = colors.findIndex((obj) => {return obj.color ==="blue";});console.log(indexOfBlue);// => 2console.log(colors[indexOfBlue]);// => { "color": "blue" }
ArrayのfindIndex
メソッドにも対となるfindLastIndex
メソッド[ES2023]があり、findLastIndex
メソッドは末尾から検索した結果が得られます。次のように、findIndex
は条件に一致する最初の要素のインデックスを返しますが、findLastIndex
は最後の要素のインデックスを返します。
// dateとcountプロパティを持つオブジェクトの配列const records = [ {date:"2020/12/1",count:5 }, {date:"2020/12/2",count:11 }, {date:"2020/12/3",count:9 }, {date:"2020/12/4",count:12 }, {date:"2020/12/5",count:3 }];// 10より大きい`count`プロパティを持つ最初のオブジェクトのインデックスを取得const firstRecordIndex = records.findIndex((record) => {return record.count >10;});// 10より大きい`count`プロパティを持つ最後のオブジェクトのインデックスを取得const lastRecordIndex = records.findLastIndex((record) => {return record.count >10;});console.log(firstRecordIndex);// => 1console.log(records[firstRecordIndex]);// => { date: "2020/12/2", count: 11 }console.log(lastRecordIndex);// => 3console.log(records[lastRecordIndex]);// => { date: "2020/12/4", count: 12 }
条件に一致する要素を取得
配列から要素を取得する方法としてインデックスを使うこともできます。先ほどのようにfindIndex
メソッドでインデックスを取得し、そのインデックスで配列へアクセスすればよいだけです。
しかし、findIndex
メソッドを使って要素を取得するケースでは、そのインデックスが欲しいのか、またはその要素自体が欲しいのかがコードとして明確ではありません。
より明確に要素自体が欲しいということを表現するには、Arrayのfind
メソッド[ES2015]が使えます。find
メソッドには、findIndex
メソッドと同様にテストする関数をコールバック関数として渡します。find
メソッドの返り値は、要素そのものとなり、要素が存在しない場合はundefined
を返します。
// colorプロパティを持つオブジェクトの配列const colors = [ {"color":"red" }, {"color":"green" }, {"color":"blue" }];// `color`プロパティが"blue"のオブジェクトを取得const blueColor = colors.find((obj) => {return obj.color ==="blue";});console.log(blueColor);// => { "color": "blue" }// 該当する要素がない場合は`undefined`を返すconst whiteColor = colors.find((obj) => {return obj.color ==="white";});console.log(whiteColor);// => undefined
find
メソッドにも対となるfindLast
メソッド[ES2023]があり、findLast
メソッドは末尾から検索した結果が得られます。次のように、find
は条件に一致した最初の要素を返しますが、findLast
は最後の要素を返します。
// dateとcountプロパティを持つオブジェクトの配列const records = [ {date:"2020/12/1",count:5 }, {date:"2020/12/2",count:11 }, {date:"2020/12/3",count:9 }, {date:"2020/12/4",count:12 }, {date:"2020/12/5",count:3 }];// 10より大きい`count`プロパティを持つ最初のオブジェクトを取得const firstRecord = records.find((record) => {return record.count >10;});// 10より大きい`count`プロパティを持つ最後のオブジェクトを取得const lastRecord = records.findLast((record) => {return record.count >10;});console.log(firstRecord);// => { date: "2020/12/2", count: 11 }console.log(lastRecord);// => { date: "2020/12/4", count: 12 }
指定範囲の要素を取得
配列から指定範囲の要素を取り出す方法としてArrayのslice
メソッドが利用できます。slice
メソッドは、第一引数の開始位置から第二引数の終了位置(終了位置の要素は含まない)までの範囲を取り出した新しい配列を返します。第二引数は省略でき、省略した場合は配列の末尾の要素まで含んだ新しい配列を返します。
const array = ["A","B","C","D","E"];// インデックス1から4まで(4の要素は含まない)の範囲を取り出すconsole.log(array.slice(1,4));// => ["B", "C", "D"]// 第二引数を省略した場合は、第一引数から末尾の要素までを取り出すconsole.log(array.slice(1));// => ["B", "C", "D", "E"]// マイナスを指定すると後ろから数えた位置となるconsole.log(array.slice(-1));// => ["E"]// 第一引数と第二引数が同じ場合は、空の配列を返すconsole.log(array.slice(1,1));// => []// 第一引数 > 第二引数の場合、常に空配列を返すconsole.log(array.slice(4,1));// => []
slice
メソッドと引数の関係を図にすると次のようになります。
真偽値を取得
最後に、指定した要素が配列に含まれているかを知る方法について見ていきます。インデックスや要素が取得できれば、その要素は配列に含まれているということはわかります。
しかし、指定した要素が含まれているかだけを知りたい場合に、ArrayのfindIndex
メソッドやfind
メソッドは過剰な機能を持っています。そのコードを読んだ人には、取得したインデックスや要素を何に使うのかが明確ではありません。
次のコードは、ArrayのindexOf
メソッドを利用し、該当する要素が含まれているかを判定しています。indexOf
メソッドの結果をindexOfJS
に代入していますが、含まれているかを判定する以外には利用していません。コードを隅々まで読まないといけないため、意図が明確ではなくコードの読みづらさにつながります。
const array = ["Java","JavaScript","Ruby"];// `indexOf`メソッドは含まれていないときのみ`-1`を返すことを利用const indexOfJS = array.indexOf("JavaScript");if (indexOfJS !== -1) {console.log("配列にJavaScriptが含まれている");// ... いろいろな処理 ...// `indexOfJS`は、含まれているのかの判定以外には利用してない}
そこで、ES2016で導入されたArrayのincludes
メソッド[ES2016]を利用します。Arrayのincludes
メソッドは配列に指定要素が含まれているかを判定します。includes
メソッドは真偽値を返すので、indexOf
メソッドを使った場合に比べて意図が明確になります。前述のコードでは次のようにincludes
メソッドを使うべきでしょう。
const array = ["Java","JavaScript","Ruby"];// `includes`は含まれているなら`true`を返すif (array.includes("JavaScript")) {console.log("配列にJavaScriptが含まれている");}
includes
メソッドは、indexOf
メソッドと同様、異なるオブジェクトだが値が同じものを見つけたい場合には利用できません。Arrayのfind
メソッドのようにテストするコールバック関数を利用して真偽値を得るには、Arrayのsome
メソッドを利用できます。
Arrayのsome
メソッドはテストするコールバック関数にマッチする要素があるならtrue
を返し、存在しない場合はfalse
を返します(「ループと反復処理」の章を参照)。
// colorプロパティを持つオブジェクトの配列const colors = [ {"color":"red" }, {"color":"green" }, {"color":"blue" }];// `color`プロパティが"blue"のオブジェクトがあるかどうかconst isIncludedBlueColor = colors.some((obj) => {return obj.color ==="blue";});console.log(isIncludedBlueColor);// => true
追加と削除
配列は可変長であるため、作成後の配列に対して要素を追加、削除できます。
要素を配列の末尾へ追加するにはArrayのpush
が利用できます。一方、末尾から要素を削除するにはArrayのpop
が利用できます。
const array = ["A","B","C"];array.push("D");// "D"を末尾に追加console.log(array);// => ["A", "B", "C", "D"]const poppedItem = array.pop();// 最末尾の要素を削除し、その要素を返すconsole.log(poppedItem);// => "D"console.log(array);// => ["A", "B", "C"]
要素を配列の先頭へ追加するにはArrayのunshift
が利用できます。一方、配列の先頭から要素を削除するにはArrayのshift
が利用できます。
const array = ["A","B","C"];array.unshift("S");// "S"を先頭に追加console.log(array);// => ["S", "A", "B", "C"]const shiftedItem = array.shift();// 先頭の要素を削除console.log(shiftedItem);// => "S"console.log(array);// => ["A", "B", "C"]
配列同士を結合
Arrayのconcat
メソッドを使うことで配列と配列を結合した新しい配列を作成できます。
const array = ["A","B","C"];const newArray = array.concat(["D","E"]);console.log(newArray);// => ["A", "B", "C", "D", "E"]
また、concat
メソッドは配列だけではなく任意の値を要素として結合できます。
const array = ["A","B","C"];const newArray = array.concat("新しい要素");console.log(newArray);// => ["A", "B", "C", "新しい要素"]
[ES2015] 配列の展開
...
(Spread構文)を使うことで、配列リテラル中に既存の配列を展開できます。
次のコードでは、配列リテラルの末尾に配列を展開しています。これは、Arrayのconcat
メソッドで配列同士を結合するのと同じ結果になります。
const array = ["A","B","C"];// Spread構文を使った場合const newArray = ["X","Y","Z", ...array];// concatメソッドの場合const newArrayConcat = ["X","Y","Z"].concat(array);console.log(newArray);// => ["X", "Y", "Z", "A", "B", "C"]console.log(newArrayConcat);// => ["X", "Y", "Z", "A", "B", "C"]
Spread構文は、concat
メソッドとは異なり、配列リテラル中の任意の位置に配列を展開できます。そのため、次のように要素の途中に配列を展開できます。
const array = ["A","B","C"];const newArray = ["X", ...array,"Z"];console.log(newArray);// => ["X", "A", "B", "C", "Z"]
[ES2019] 配列をフラット化
Arrayのflat
メソッド[ES2019]を使うことで、多次元配列をフラットな配列に変換できます。引数を指定しなかった場合は1段階のみのフラット化ですが、引数に渡す数値でフラット化する深さを指定できます。配列をすべてフラット化する場合には、無限を意味するInfinity
を値として渡すことで実現できます。
const array = [[["A"],"B"],"C"];// 引数なしは1を指定した場合と同じconsole.log(array.flat());// => [["A"], "B", "C"]console.log(array.flat(1));// => [["A"], "B", "C"]console.log(array.flat(2));// => ["A", "B", "C"]// すべてをフラット化するにはInfinityを渡すconsole.log(array.flat(Infinity));// => ["A", "B", "C"]
また、Arrayのflat
メソッドは必ず新しい配列を作成して返すメソッドです。そのため、これ以上フラット化できない配列をフラット化しても、同じ要素を持つ新しい配列を返します。
const array = ["A","B","C"];console.log(array.flat());// => ["A", "B", "C"]
配列から要素を削除
Array.prototype.splice
配列の先頭や末尾の要素を削除する場合はArrayのshift
メソッドやpop
メソッドで行えます。しかし、配列の任意のインデックスの要素を削除できません。配列の任意のインデックスの要素を削除するにはArrayのsplice
メソッドを利用できます。
splice
メソッドを利用すると、削除した要素を自動で詰めることができます。splice
メソッドは指定したインデックスから、指定した数だけ要素を取り除き、必要ならば要素を同時に追加できます。
const array = [];array.splice(インデックス, 削除する要素数);// 削除と同時に要素の追加もできるarray.splice(インデックス, 削除する要素数, ...追加する要素);
たとえば、配列のインデックスが1
の要素を削除するには、インデックス1
から1
つの要素を削除するという指定をする必要があります。このとき、削除した要素は自動で詰められるため、疎な配列にはなりません。
const array = ["a","b","c"];// 1番目から1つの要素("b")を削除array.splice(1,1);console.log(array);// => ["a", "c"]console.log(array.length);// => 2console.log(array[1]);// => "c"// すべて削除array.splice(0, array.length);console.log(array.length);// => 0
length
プロパティへの代入
配列のすべての要素を削除することはArrayのsplice
で行えますが、配列のlength
プロパティへの代入を利用した方法もあります。
const array = [1,2,3];array.length =0;// 配列を空にするconsole.log(array);// => []
配列のlength
プロパティへ要素数
を代入すると、その要素数に配列が切り詰められます。つまり、length
プロパティへ0
を代入すると、インデックスが0
以降の要素がすべて削除されます。
空の配列を代入
最後に、その配列の要素を削除するのではなく、新しい空の配列を変数へ代入する方法です。次のコードでは、array
変数に空の配列を代入することで、array
に空の配列を参照させられます。
let array = [1,2,3];console.log(array.length);// => 3// 新しい配列で変数を上書きarray = [];console.log(array.length);// => 0
元々、array
変数が参照していた[1, 2, 3]
はどこからも参照されなくなり、ガベージコレクションによりメモリから解放されます。
また、const
で宣言した配列の場合は変数に対して再代入できないため、この手法は使えません。そのため、再代入をしたい場合はlet
またはvar
で変数宣言をする必要があります。
const array = [1,2,3];console.log(array.length);// => 3// `const`で宣言された変数には再代入できないarray = [];// TypeError: invalid assignment to const `array' が発生
破壊的なメソッドと非破壊的なメソッド
これまで紹介してきた配列を変更するメソッドには、破壊的なメソッドと非破壊的メソッドがあります。この破壊的なメソッドと非破壊的メソッドの違いを知ることは、意図しない結果を避けるために重要です。
破壊的なメソッド(Mutable Method)とは、配列オブジェクトそのものを変更し、変更した配列または変更箇所を返すメソッドです。非破壊的メソッド(Immutable Method)とは、配列オブジェクトのコピーを作成してから変更し、そのコピーした配列を返すメソッドです。
破壊的なメソッドの例として、配列に要素を追加するArrayのpush
メソッドがあります。push
メソッドは、myArray
の配列そのものへ要素を追加しています。その結果myArray
変数の参照する配列が変更されるため破壊的なメソッドです。
const myArray = ["A","B","C"];const result = myArray.push("D");// `push`の返り値は配列ではなく、追加後の配列のlengthconsole.log(result);// => 4// `myArray`が参照する配列そのものが変更されているconsole.log(myArray);// => ["A", "B", "C", "D"]
非破壊的なメソッドの例として、配列に要素を結合するArrayのconcat
メソッドがあります。concat
メソッドは、myArray
をコピーした配列に対して要素を結合し、その配列を返します。myArray
変数の参照する配列は変更されないため非破壊的なメソッドです。
const myArray = ["A","B","C"];// `concat`の返り値は結合済みの新しい配列const newArray = myArray.concat("D");console.log(newArray);// => ["A", "B", "C", "D"]// `myArray`は変更されていないconsole.log(myArray);// => ["A", "B", "C"]// `newArray`と`myArray`は異なる配列オブジェクトconsole.log(myArray === newArray);// => false
JavaScriptにおいて破壊的なメソッドと非破壊的メソッドを名前から見分けるのは難しいという問題があります。また、配列を返す破壊的なメソッドもあるため、返り値からも判別できません。たとえば、Arrayのsort
メソッドは返り値がソート済みの配列ですが破壊的メソッドです。
次の表で紹介するメソッドは破壊的なメソッドです。
メソッド名 | 返り値 |
---|---|
Array.prototype.pop | 配列の末尾の値 |
Array.prototype.push | 変更後の配列のlength |
Array.prototype.splice | 取り除かれた要素を含む配列 |
Array.prototype.reverse | 反転した配列 |
Array.prototype.shift | 配列の先頭の値 |
Array.prototype.sort | ソートした配列 |
Array.prototype.unshift | 変更後の配列のlength |
Array.prototype.copyWithin [ES2015] | 変更後の配列 |
Array.prototype.fill [ES2015] | 変更後の配列 |
破壊的メソッドは意図せぬ副作用を与えてしまうことがあるため、そのことを意識して利用する必要があります。たとえば、配列から特定のインデックスの要素を削除するremoveAtIndex
という関数を提供したいとします。
// `array`の`index`番目の要素を削除した配列を返す関数functionremoveAtIndex(array, index) {/* 実装 */ }
次のように、破壊的なメソッドであるArrayのsplice
メソッドで要素を削除すると、引数として受け取った配列にも影響を与えます。この場合removeAtIndex
関数には副作用があるため、破壊的であることについてのコメントがあると親切です。
// `array`の`index`番目の要素を削除した配列を返す関数// 引数の`array`は破壊的に変更されるfunctionremoveAtIndex(array, index) { array.splice(index,1);return array;}const array = ["A","B","C"];// `array`から1番目の要素を削除した配列を取得const newArray =removeAtIndex(array,1);console.log(newArray);// => ["A", "C"]// `array`自体にも影響を与えるconsole.log(array);// => ["A", "C"]
一方、非破壊的メソッドは配列のコピーを作成するため、元々の配列に対して影響はありません。このremoveAtIndex
関数を非破壊的なものにするには、受け取った配列をコピーしてから変更を加える必要があります。
JavaScriptにはcopy
メソッドそのものは存在しませんが、配列をコピーする方法としてArrayのslice
メソッドとconcat
メソッドが利用されています。slice
メソッドとconcat
メソッドは引数なしで呼び出すと、その配列のコピーを返します。
const myArray = ["A","B","C"];// `slice`は`myArray`のコピーを返す - `myArray.concat()`でも同じconst copiedArray = myArray.slice();myArray.push("D");console.log(myArray);// => ["A", "B", "C", "D"]// `array`のコピーである`copiedArray`には影響がないconsole.log(copiedArray);// => ["A", "B", "C"]// コピーであるため参照は異なるconsole.log(copiedArray === myArray);// => false
コピーした配列に変更を加えることで、removeAtIndex
関数を非破壊的な関数として実装できます。非破壊的であれば引数の配列への副作用がないので、注意させるようなコメントは不要です。
// `array`の`index`番目の要素を削除した配列を返す関数functionremoveAtIndex(array, index) {// コピーを作成してから変更するconst copiedArray = array.slice(); copiedArray.splice(index,1);return copiedArray;}const array = ["A","B","C"];// `array`から1番目の要素を削除した配列を取得const newArray =removeAtIndex(array,1);console.log(newArray);// => ["A", "C"]// 元の`array`には影響がないconsole.log(array);// => ["A", "B", "C"]
このようにJavaScriptの配列には破壊的なメソッドと非破壊的メソッドが混在しています。名前からも区別することが難しく、副作用を避けるためにコピーを作ってから破壊的メソッドを使うというパターンが利用されていました。
しかし、ES2023でこの状況を改善する変更が追加されています。今まで、破壊的なメソッドしかなかった、splice
、reverse
、sort
に対して、非破壊的なバージョンであるtoSpliced
、toReversed
、toSorted
が追加されました。
これらのto
から始まる非破壊的メソッドが受け取る引数は破壊的なメソッドと同じですが、非破壊的に変更した配列を返す点が異なります。次のコードのtoSpliced
メソッドは、配列を複製してから変更するため、元々の配列であるarray
には影響を与えていないことがわかります。
const array = ["A","B","C"];// `toSpliced`は`array`を複製してから変更するconst newArray = array.toSpliced(1,1);console.log(newArray);// => ["A", "C"]// コピー元の`array`には影響がないconsole.log(array);// => ["A", "B", "C"]
先ほどremoveAtIndex
関数の実装では、slice
メソッドで配列をコピーしてからsplice
メソッドを呼び出していました。次のコードでは、toSpliced
メソッドを使うことで、より簡潔に非破壊的なremoveAtIndex
関数を実装しています。
// `array`の`index`番目の要素を削除した配列を返す関数functionremoveAtIndex(array, index) {// コピーを作成してから変更するreturn array.toSpliced(index,1);}const array = ["A","B","C"];// `array`から1番目の要素を削除した配列を取得const newArray =removeAtIndex(array,1);console.log(newArray);// => ["A", "C"]// 元の`array`には影響がないconsole.log(array);// => ["A", "B", "C"]
また、ES2023では配列の指定したインデックスの要素を非破壊的に変更するwith
メソッドも追加されました。array[index] = value
の代入処理は、元々の配列を変更する破壊的な処理です。 これに対してwith
メソッドは、配列を複製してから指定したインデックスの要素を変更した配列を返す非破壊的なメソッドです。
const array = ["A","B","C"];// `array`の1番目の要素を変更した配列を返すconst newArray = array.with(1,"B2");console.log(newArray);// => ["A", "B2", "C"]
次の表では、破壊的な方法に対応する非破壊的な方法をまとめています。
破壊的な方法 | 非破壊な方法 |
---|---|
array[index] = item | Array.prototype.with [ES2023] |
Array.prototype.pop | array.slice(0, -1) とarray.at(-1) [ES2022] |
Array.prototype.push | [...array, item] [ES2015] |
Array.prototype.splice | Array.prototype.toSpliced [ES2023] |
Array.prototype.reverse | Array.prototype.toReversed [ES2023] |
Array.prototype.sort | Array.prototype.toSorted [ES2023] |
Array.prototype.shift | array.slice(1) とarray.at(0) [ES2022] |
Array.prototype.unshift | [item, ...array] [ES2015] |
Array.prototype.copyWithin [ES2015] | なし |
Array.prototype.fill [ES2015] | なし |
破壊的なメソッドは、シンプルですが元の配列も変更してしまうため、意図しない副作用が発生しバグの原因となる可能性があります。非破壊的なメソッドは、使い分けが必要ですが元の配列を変更せずに新しい配列を返すため、副作用が発生することはありません。
そのため、まず非破壊的な方法で書けるかを検討し、そうではない場合に破壊的な方法を利用するとよいでしょう。
配列を反復処理するメソッド
「ループと反復処理」の章において配列を反復処理する方法を一部解説しましたが、改めて関連するArrayメソッドを見ていきます。反復処理の中でもよく利用されるのがArrayのforEach
、map
、filter
、reduce
メソッドです。どのメソッドも共通して引数にコールバック関数を受け取るため高階関数と呼ばれます。
Array.prototype.forEach
ArrayのforEach
メソッドは配列の要素を先頭から順番にコールバック関数へ渡し、反復処理を行うメソッドです。
次のようにコールバック関数には要素, インデックス, 配列
が引数として渡され、配列要素の先頭から順番に反復処理します。
const array = [1,2,3];array.forEach((currentValue, index, array) => {console.log(currentValue, index, array);});// コンソールの出力// 1, 0, [1, 2, 3]// 2, 1, [1, 2, 3]// 3, 2, [1, 2, 3]
Array.prototype.map
Arrayのmap
メソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数が返した値から新しい配列を返す非破壊的なメソッドです。配列の各要素を加工したい場合に利用します。
次のようにコールバック関数には要素, インデックス, 配列
が引数として渡され、配列要素の先頭から順番に反復処理します。map
メソッドの返り値は、それぞれのコールバック関数が返した値を集めた新しい配列です。
const array = [1,2,3];// 各要素に10を乗算した新しい配列を作成するconst newArray = array.map((currentValue, index, array) => {return currentValue *10;});console.log(newArray);// => [10, 20, 30]// 元の配列とは異なるインスタンスconsole.log(array === newArray);// => false
Array.prototype.filter
Arrayのfilter
メソッドは配列の要素を順番にコールバック関数へ渡し、コールバック関数がtrue
を返した要素だけを集めた新しい配列を返す非破壊的なメソッドです。配列から不要な要素を取り除いた配列を作成したい場合に利用します。
次のようにコールバック関数には要素, インデックス, 配列
が引数として渡され、配列要素の先頭から順番に反復処理します。filter
メソッドの返り値は、コールバック関数がtrue
を返した要素だけを集めた新しい配列です。
const array = [1,2,3];// 奇数の値を持つ要素だけを集めた配列を返すconst newArray = array.filter((currentValue, index, array) => {return currentValue %2 ===1;});console.log(newArray);// => [1, 3]// 元の配列とは異なるインスタンスconsole.log(array === newArray);// => false
Array.prototype.reduce
Arrayのreduce
メソッドは累積値(アキュムレータ)と配列の要素を順番にコールバック関数へ渡し、1つの累積値を返します。配列から配列以外を含む任意の値を作成したい場合に利用します。
ここまでで紹介した反復処理のメソッドとは異なり、コールバック関数には累積値, 要素, インデックス, 配列
を引数として渡します。reduce
メソッドの第二引数には累積値
の初期値となる値を渡せます。
次のコードでは、reduce
メソッドは初期値を0として配列の各要素を加算した1つの数値を返します。つまり配列から配列要素の合計値というNumber型の値を返しています。
const array = [1,2,3];// すべての要素を加算した値を返す// accumulatorの初期値は`0`const totalValue = array.reduce((accumulator, currentValue, index, array) => {return accumulator + currentValue;},0);// 0 + 1 + 2 + 3という式の結果が返り値になるconsole.log(totalValue);// => 6
reduce
メソッドに渡したコールバック関数は配列の要素数である3回呼び出され、それぞれ次のような結果になります。
accumulator | currentValue | returnした値 | |
---|---|---|---|
1回目の呼び出し | 0 | 1 | 0 + 1 |
2回目の呼び出し | 1 | 2 | 1 + 2 |
3回目の呼び出し | 3 | 3 | 3 + 3 |
Arrayのreduce
メソッドはやや複雜ですが、配列から配列以外のデータ型の値を作成できる特徴があります。また、reduce
メソッドでは、配列から直接Number型の値を返せるため、totalValue
という変数を再代入できないconst
で宣言していました。
配列の数値の合計をforEach
メソッドなど反復処理で計算すると、次のコードのようにtotalValue
という変数は再代入ができるlet
で宣言する必要があります。
const array = [1,2,3];// 初期値は`0`let totalValue =0;array.forEach((currentValue) => { totalValue += currentValue;});console.log(totalValue);// => 6
let
で宣言した変数は再代入が可能なため、意図しない箇所で変数の値が変更され、バグの原因となることがあります。そのため、できる限り変数をconst
で宣言したい場合にはreduce
メソッドは有用です。一方で、reduce
メソッドは可読性があまりよくないため、コードの意図が伝わりにくいというデメリットもあります。
reduce
メソッドには利点と可読性のトレードオフがありますが、利用する場合はreduce
メソッドを扱う処理を関数にするといった処理の意図がわかるように工夫をする必要があります。
const array = [1,2,3];functionsum(array) {return array.reduce((accumulator, currentValue) => {return accumulator + currentValue; },0);}console.log(sum(array));// => 6
[ES2024]Object.groupBy
静的メソッド
Array.prototype.reduce
メソッドを使うことで、配列から数値やオブジェクトなど任意の値を作成できます。
先ほどは配列の合計の数値を計算する例でしたが、配列からオブジェクトを作成することもできます。配列からオブジェクトを作成したいユースケースとして、配列の要素を条件によってグループ分けしたいケースがあります。たとえば、数値からなる配列の要素を奇数と偶数の配列に分けたい場合などです。
Array.prototype.reduce
メソッドを使って、数値からなる配列を奇数と偶数に分けるコードは次のようになります。
const array = [1,2,3,4,5];const grouped = array.reduce((accumulator, currentValue) => {// 2で割った余りが0なら偶数(even)、そうでないなら奇数(odd)const key = currentValue %2 ===0 ?"even" :"odd";if (!accumulator[key]) { accumulator[key] = []; }// グループ分けしたキーの配列に要素を追加 accumulator[key].push(currentValue);return accumulator;}, {});console.log(grouped.even);// => [2, 4]console.log(grouped.odd);// => [1, 3, 5]
しかし、reduce
メソッドは使い方がやや複雜であるため、可能なら避けたほうが読みやすいコードとなりやすいです。ES2024では、Object.groupBy
静的メソッドが追加され、配列からグループ分けしたオブジェクトを作成できるようになっています。
Object.groupBy
静的メソッド1には、第一引数に配列などのiterableオブジェクト、第二引数にグループ分けの条件を返すコールバック関数を渡します。第二引数のコールバック関数が返した値をキーとして、配列の要素をグループ分けしたオブジェクトが作成されます。
先ほどのコードをObject.groupBy
静的メソッドを使って書き換えると、次のようになります。
const array = [1,2,3,4,5];const grouped =Object.groupBy(array,(currentValue) => {// currentValueが偶数なら"even"、そうでないなら"odd"の配列に追加されるreturn currentValue %2 ===0 ?"even" :"odd";});console.log(grouped.even);// => [2, 4]console.log(grouped.odd);// => [1, 3, 5]
Object.groupBy
静的メソッドを使うことで、配列からグループ分けしたオブジェクトを簡潔に作成できます。
[コラム] Array-likeオブジェクト
配列のように扱えるが配列ではないオブジェクトのことを、Array-likeオブジェクトと呼びます。Array-likeオブジェクトとは配列のようにインデックスにアクセスでき、配列のようにlength
プロパティも持っています。しかし、配列のインスタンスではないため、Arrayのプロトタイプメソッドを持っていないオブジェクトのことです。
機能 | Array-likeオブジェクト | 配列 |
---|---|---|
インデックスアクセス(array[0] ) | できる | できる |
長さ(array.length ) | 持っている | 持っている |
Arrayのプロトタイプメソッド(forEach メソッドなど) | 持っていない場合もある | 持っている |
Array-likeオブジェクトの例としてarguments
があります。arguments
オブジェクトは、function
で宣言した関数の中から参照できる変数です。arguments
オブジェクトには関数の引数に渡された値が順番に格納されていて、配列のように引数へアクセスできます。
functionmyFunc() {console.log(arguments[0]);// => "a"console.log(arguments[1]);// => "b"console.log(arguments[2]);// => "c"// 配列ではないため、配列のメソッドは持っていないconsole.log(typeofarguments.forEach);// => "undefined"}myFunc("a","b","c");
Array-likeオブジェクトか配列なのかを判別するにはArray.isArray
静的メソッドを利用できます。Array-like
オブジェクトは配列ではないので結果は常にfalse
となります。
functionmyFunc() {console.log(Array.isArray([1,2,3]));// => trueconsole.log(Array.isArray(arguments));// => false}myFunc("a","b","c");
Array-likeオブジェクトは配列のようで配列ではないというもどかしさを持つオブジェクトです。Array.from
静的メソッド[ES2015]を使うことでArray-likeオブジェクトを配列に変換して扱うことができます。一度配列に変換してしまえばArrayメソッドも利用できます。
functionmyFunc() {// Array-likeオブジェクトを配列へ変換const argumentsArray =Array.from(arguments);console.log(Array.isArray(argumentsArray));// => true// 配列のメソッドを利用できる argumentsArray.forEach(arg => {console.log(arg); });}myFunc("a","b","c");
メソッドチェーンと高階関数
配列で頻出するパターンとしてメソッドチェーンがあります。メソッドチェーンとは、メソッドを呼び出した返り値に対してさらにメソッド呼び出しをするパターンのことを言います。
次のコードでは、Arrayのconcat
メソッドの返り値、つまり配列に対してさらにconcat
メソッドを呼び出すというメソッドチェーンが行われています。
const array = ["a"].concat("b").concat("c");console.log(array);// => ["a", "b", "c"]
このコードのconcat
メソッドの呼び出しを分解してみると何が行われているのかわかりやすいです。concat
メソッドの返り値は結合した新しい配列です。先ほどのメソッドチェーンでは、その新しい配列に対してさらにconcat
メソッドで値を結合しているということがわかります。
// メソッドチェーンを分解した例// 一時的な`abArray`という変数が増えているconst abArray = ["a"].concat("b");console.log(abArray);// => ["a", "b"]const abcArray = abArray.concat("c");console.log(abcArray);// => ["a", "b", "c"]
メソッドチェーンを利用することで処理の見た目を簡潔にできます。メソッドチェーンを利用した場合も最終的な処理結果は同じですが、途中の一時的な変数を省略できます。先ほどの例ではabArray
という一時的な変数をメソッドチェーンでは省略できています。
メソッドチェーンは配列に限ったものではありませんが、配列では頻出するパターンです。なぜなら、配列に含まれるデータを表示する際には、最終的に文字列や数値など別のデータへ加工することがほとんどであるためです。配列には配列を返す高階関数が多く実装されているため、配列を柔軟に加工できます。
次のコードでは、ECMAScriptのバージョン名と発行年数が定義されたECMAScriptVersions
という配列が定義されています。この配列から2000
年以前に発行されたECMAScriptのバージョン名の一覧を取り出すことを考えてみます。目的の一覧を取り出すには「2000年以前のデータに絞り込む」と「データからname
を取り出す」という2つの加工処理を組み合わせる必要があります。
この2つの加工処理はArrayのfilter
メソッドとmap
メソッドで実現できます。filter
メソッドで配列から2000
年以前というルールで絞り込み、map
メソッドでそれぞれの要素からname
プロパティを取り出せます。どちらのメソッドも配列を返すのでメソッドチェーンで処理をつなげられます。
// ECMAScriptのバージョン名と発行年constECMAScriptVersions = [ {name:"ECMAScript 1",year:1997 }, {name:"ECMAScript 2",year:1998 }, {name:"ECMAScript 3",year:1999 }, {name:"ECMAScript 5",year:2009 }, {name:"ECMAScript 5.1",year:2011 }, {name:"ECMAScript 2015",year:2015 }, {name:"ECMAScript 2016",year:2016 }, {name:"ECMAScript 2017",year:2017 },];// メソッドチェーンで必要な加工処理を並べているconst versionNames =ECMAScriptVersions// 2000年以下のデータに絞り込み .filter(ECMAScript =>ECMAScript.year <=2000)// それぞれの要素から`name`プロパティを取り出す .map(ECMAScript =>ECMAScript.name);console.log(versionNames);// => ["ECMAScript 1", "ECMAScript 2", "ECMAScript 3"]
メソッドチェーンを使うことで複数の処理からなるものをひとつのまとまった処理のように見せることができます。長過ぎるメソッドチェーンは長過ぎる関数と同じように読みにくくなりますが、適度な単位のメソッドチェーンは処理をスッキリ見せるパターンとして利用されています。
まとめ
この章では配列について学びました。
- 配列は順序を持った要素を格納できるオブジェクトの一種
- 配列には破壊的なメソッドと非破壊的なメソッドがある
- 配列には反復処理を行う高階関数となるメソッドがある
- メソッドチェーンは配列のメソッドが配列を返すことを利用している
配列はJavaScriptの中でもよく使われるオブジェクトで、メソッドの種類も多いです。この書籍でもすべてのメソッドは紹介していないため、詳しくはArrayについてのドキュメントも参照してみてください。
1.Array.prototype.groupBy
メソッドのようなArrayのメソッドではないのは、同じメソッド名を実装するウェブサイトが多く存在しており後方互換性がなかったためです。 ↩