JavaScript で配列を反復する
この記事では、JavaScript で配列を反復処理するいろいろな方法について 1 つ 1 つ解説します。
ES2024 に基づいて解説をします。ここで紹介する構文・メソッド・コード例の一部は古い処理系では動作しないことがありますので必要に応じて Can I use... などでそれぞれの対応状況を確認してください。また、それより新しく標準化された(される)反復処理の方法についてはこの記事でカバーされていない可能性がありますのでご注意ください。
用語の定義
この記事においては、次の意味で用語を使用します。
反復処理について
- ループ (loop) は、同じコードが繰り返し実行されるような構造を指します。
- イテレーション (iteration) は、繰り返しの各ステップを指します。
したがって、1 個の「ループ」が 1 回実行されるとき、複数回の「イテレーション」が発生する(ことがある)、という形になります。
配列について
-
配列 (array) は、JavaScript のオブジェクトの一種で、
Array.isArray(o)
の値が true になるようなo
の値を指します。- たとえば
["a", "b", "c"]
のように角括弧 ([
]
) で要素を囲んだ式の値は配列です。 - TypedArray は配列ではありません。(この記事では TypedArray については割愛します)
- たとえば
-
配列風オブジェクト (array-like object) は、JavaScript のオブジェクトのうち、式
+o.length
を評価するときに実行時エラーが発生しないようなo
の値を指します。- 狭義には、
length
というキーのプロパティを持ち、その値(長さと呼ぶ)が 0 以上 以下の数値型 (Number) の整数であるようなオブジェクトを指します。2^{53} - 1 - 配列風オブジェクトの「長さ」を言及する文脈では、この狭義で「配列風オブジェクト」を使います。
- 通常、0 以上長さ未満の整数(を文字列化したもの)をキーとするプロパティを持つとみなされます。
- たとえば式
{ 0: "a", 1: "b", 2: "c", length: 3 }
の値は配列風オブジェクトです(が、配列ではありません)。 - 配列も(狭義の)配列風オブジェクトの一種です。
- 狭義には、
密・疎について
- 配列が密 (dense) であるとは、0 以上長さ未満のすべての整数について、それ(を文字列化したもの)をキーとするプロパティが存在することをいいます。
- たとえば式
["a", "b", "c"]
の値は密な配列です。
- たとえば式
- 配列が疎 (sparse) であるとは、それが密でない、つまり 0 以上長さ未満の整数のうち、それ(を文字列化したもの)をキーとするプロパティを持たない整数が 1 つ以上存在することをいいます。
- たとえば式
["a", , "c"]
の値は、長さが 3 だが1
をキーとするプロパティが存在しない(空になっている)ため疎な配列です。
- たとえば式
「だいたい同じ意味」について
- 2 つのコード例に対する「だいたい同じ意味」という表現は、配列や
Array.prototype
の(要素以外の)プロパティを書き換えている場合や、Array
をサブクラス化している場合など、厳密には挙動が異なるケースが考えられるが、通常範囲のコードでは実用上は同じ挙動になるであろうというニュアンスで使っています。通常のコードでも挙動に差が生まれると思われるケースについては、その都度注意書きを入れています。また対象のコードについて、処理中にエラーが発生しないことを前提としています。
for 文
for 文は、反復処理を記述するための構文の 1 つです。
JavaScript の for 文は大きく 3 種類に分けることができます。
伝統的な for 文
C 言語などにもある形式の、丸括弧の中にセミコロン (;
) を 2 つ使った for 文です。
for (<初期化>; <条件>; <更新>)
<文>
伝統的な for 文が実行されるとき、次のような順序で処理が行われます。
-
<初期化>
の式または宣言が評価されます。 -
<条件>
の式が評価されます。- 評価結果が falsy ならば、ループが終了し、実行が for 文の直後に移ります。
-
<文>
が実行されます。 -
<更新>
の式が評価されます。 - 2 に戻ります。
構文要素の省略
<初期化>
, <条件>
, <更新>
の部分はそれぞれ省略が可能です。
-
<初期化>
を省略すると、単に初期化処理が行われず、for 文の実行は<条件>
の評価から始まります。 -
<条件>
を省略すると、条件の評価がスキップされ、<初期化>
または<更新>
の処理のあとに無条件で<文>
が実行されます。- 無限ループを作るために使うことがあります。
- ループの終了条件が複雑な場合、この部分を省略して、
<文>
の中で break 文を使うことがあります。-
for (<初期化>; <条件>; <更新>) <文>
はfor (<初期化>; ; <更新>) { if (!(<条件>)) break; <文> }
と同じ意味になります。
さらに if 文の部分を複数の if 文に分割することで、複雑な条件を読みやすくすることができます。
-
-
<更新>
を省略すると、単に更新処理が行われず、<文>
の実行のあとに<条件>
の評価が行われます。
初期化の記述方法
<初期化>
の部分は、細かく分けて 3 種類の記述方法があります。
- 省略する場合(上述)
- 変数を宣言する場合:
let i = 0
など- 通常の let 宣言と同様、複数の変数を同時に宣言することもできます。
for (let i = 0, j = 0; ...; ...) ...
- 変数の初期値を省略することもできます。
for (let i, j; ...; ...) ...
- 構文上は
var i = 0
やconst i = 0
を使うこともできます。-
var
は古い構文です。for 文を含んでいる関数のブロック全体をスコープとする変数を宣言します。 -
const
は再代入不可の変数を宣言します。典型的なループの場合、インデックス変数を更新するため、const
ではなくlet
を使うことが多いです。 - 1 箇所の
<初期化>
にlet
var
const
の 2 種類以上を混在させることはできません。
-
-
let
とconst
は for 文をスコープとする変数を宣言します。- ここで宣言された変数は、for 文の外では参照できません。
-
let
で変数を宣言した場合、イテレーションが終わるごとに(<更新>
の直前で)この変数が複製されます。
イテレーションが終了したあとにもこの変数が参照される場合に、イテレーションごとに別々の値を保持できるようになります。// var の場合 var array = ["a", "b", "c"]; function foo() { for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i, array[i]), 10); // 同じ変数 i を参照している // `console.log(i)` が実行されるときには i の値が 3 になっている } } foo(); // 出力: // 3 undefined // 3 undefined // 3 undefined
// let の場合 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i, array[i]), 10); // 各イテレーションごとに別々の変数 i を参照している } // 出力: // 0 a // 1 b // 2 c
- 通常の let 宣言と同様、複数の変数を同時に宣言することもできます。
- 1 つの式を記述する場合
-
i = 0
のような代入式を記述することもできます。この場合、変数i
は別途宣言されている必要があります。
-
典型的な配列の反復は、次のような形になります。
const array = ["a", "b", "c", "d", "e"];
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
// 出力:
// a
// b
// c
// d
// e
-
<初期化>
:let i = 0
で、このループのための変数を 0 に初期化します。- 変数には
i
やindex
などの名前をつけることが多いです。 - ここで 0 を使うのは、配列のインデックスが 0 から始まるためです。
- 変数には
-
<条件>
:i < array.length
で、変数が配列の長さよりも小さいかどうかを確認します。-
<更新>
で変数を 1 ずつ増やすので、配列の最後の要素の処理が終了するとi
がarray.length
と等しくなり、この条件の評価結果が false になってループが終了します。
-
-
<更新>
:i++
で、変数を 1 ずつ増やします。-
++i
,i += 1
,i = i + 1
のように書くこともあります。 -
i++
は後置インクリメント、++i
は前置インクリメントと呼ばれます。(詳細はこの記事では割愛します)
-
-
<文>
- 変数
i
を配列のインデックスとして使うことができます。 -
array[i]
で、配列の要素を 1 つずつ扱うことができます。
- 変数
伝統的な for 文の特徴
伝統的な for 文では配列のインデックス i
を変数として直接操作するため、ループの仕方について自由度が高く、複雑なループを構成できます。
- 配列を逆順に反復することができます。
for (let i = array.length - 1; i >= 0; i--) ...
- 配列の一部の範囲に限って反復することができます。
- 配列の先頭と末尾の 1 要素ずつを除いた範囲に対して処理を行う場合
for (let i = 1; i < array.length - 1; i++) ...
- 配列の先頭と末尾の 1 要素ずつを除いた範囲に対して処理を行う場合
- 1 つおきなどの反復処理をすることができます。
for (let i = 0; i < array.length; i += 2) ...
-
array[i]
とarray[i + 1]
のように、要素を 2 つずつまとめて処理する際に便利です。(ただし配列の要素数が奇数の場合の処理には注意が必要です)
-
- イテレーションの途中で
i
の値を自由に変更することができます。- イテレーション内の条件によって配列の次の要素の処理をスキップする場合
const array = ["a", "b", "c", "d", "e"]; for (let i = 0; i < array.length; i++) { console.log(array[i]); if (array[i] === "b") { i += 1; } } // 出力: // a // b // d // e
- イテレーション内の条件によって配列の次の要素の処理をスキップする場合
また、単純な反復処理においても、配列のインデックスを参照できることによるメリットがときどきあります。
- 配列のインデックスを処理に使う場合
const array = ["a", "b", "c", "d", "e"]; for (let i = 0; i < array.length; i++) { console.log(array[i].repeat(i + 1)); } // 出力: // a // bb // ccc // dddd // eeeee
- 配列の要素を更新する場合
const array = ["a", "b", "c", "d", "e"]; for (let i = 0; i < array.length; i++) { if (array[i] === "c") { array[i] = "javascript"; } } console.log(array); // 出力: [ 'a', 'b', 'javascript', 'd', 'e' ]
- 2 つ以上の配列に対して同時に処理を行う場合
const array1 = ["a", "b", "c"]; const array2 = ["x", "y", "z"]; for (let i = 0; i < array1.length; i++) { console.log(array1[i], array2[i]); } // 出力: // a x // b y // c z
伝統的な for 文は配列に限らず配列風オブジェクトに対しても同じように反復処理を行うことができます。
伝統的な for 文の書き換え
<文>
に continue 文が含まれない場合、
for (<初期化>; <条件>; <更新>)
<文>
は
{
<初期化>;
while (<条件>) {
<文>
<更新>;
}
}
とだいたい同じ意味になります。(<条件>
を省略している場合は while (true)
)
ただし、<初期化>
の let
で宣言された変数がイテレーションごとに複製される挙動がこの書き換えで考慮されていないため、let
で宣言された変数をイテレーションの終了後も使っている場合は注意が必要です。
array
が配列のとき、
for (let i = 0; i < array.length; i++) {
const element = array[i];
<処理>
}
は <処理>
の中で array
や i
を参照していない場合、
for (const element of array) {
<処理>
}
と書き換えることができます。
array
が配列のとき、逆順のループ
for (let i = array.length - 1; i >= 0; i--) {
const element = array[i];
<処理>
}
は <処理>
の中で array
や i
を参照していない場合、
for (const element of array.toReversed()) {
<処理>
}
と書き換えることができます。ただし toReversed メソッドは比較的新しく追加されたメソッドのため、古い処理系でも動作させるために array.slice(0).reverse()
などを使うこともあります。
この書き換えは array
を逆順にした新しい配列を中間で作るために効率が悪化する可能性があります。
新しい配列を作るパターン
典型的なパターンとして、array
が配列のとき
const newArray = [];
for (let i = 0; i < array.length; i++) {
const element = array[i];
const newElement = <式>;
newArray.push(newElement);
}
は <式>
が i
array
newArray
を参照せず、yield 演算子や await 演算子を含まない場合、
const newArray = array.map((element) => {
const newElement = <式>;
return newElement;
});
とだいたい同じ意味になります。
push する条件付きの場合は、
const newArray = [];
for (let i = 0; i < array.length; i++) {
const element = array[i];
if (<条件>) {
const newElement = <式>;
newArray.push(newElement);
}
}
は <条件>
<式>
が i
array
newArray
を参照せず、yield 演算子や await 演算子を含まない場合、
const newArray = array.flatMap((element) => {
if (<条件>) {
const newElement = <式>;
return [newElement];
}
return [];
});
とだいたい同じ意味になります。
要素を探すパターン
条件を満たす要素を見つける場合は、
let foundElement = undefined;
for (let i = 0; i < array.length; i++) {
const element = array[i];
if (<条件>) {
foundElement = element;
break;
}
}
は <条件>
が i
array
foundElement
を参照せず、yield 演算子や await 演算子を含まない場合、
let foundElement = array.find((element) => {
return <条件>;
});
とだいたい同じ意味になります。とくに、直接値を返す場合
for (let i = 0; i < array.length; i++) {
const element = array[i];
if (<条件>) {
return element;
}
}
return undefined;
は
return array.find((element) => {
return <条件>;
});
とだいたい同じ意味になります。
インデックスが必要な場合は、
let i;
for (i = 0; i < array.length; i++) {
const element = array[i];
if (<条件>) {
break;
}
}
if (i === array.length) {
<見つからなかった場合の処理>
} else {
<見つかった場合の i を使う処理>
}
は
const i = array.findIndex((element) => {
return <条件>;
});
if (i === -1) {
<見つからなかった場合の処理>
} else {
<見つかった場合の i を使う処理>
}
とだいたい同じ意味になります。
for-of 文
配列の要素を直接反復するための for 文です。
for (const <変数> of <反復対象>)
<文>
of より前の部分について
const
の代わりに let
や var
を使って変数を宣言することもできます。
-
var
は古い構文です。for 文を含んでいる関数のブロック全体をスコープとする変数を宣言します。 -
let
やconst
は for 文をスコープとする変数を宣言します。- ここで宣言された変数は、for 文の外では参照できません。
- イテレーションごとに別々の値を保持できます。
また、変数を宣言せずに代入だけを行うことも構文上はできます。(個人的にはあまり見ない書き方です)
const array = ["a", "b", "c", "d", "e"];
let element;
for (element of array) {
console.log(element);
}
典型的な配列の反復は、次のような形になります。
const array = ["a", "b", "c", "d", "e"];
for (const element of array) {
console.log(element);
}
// 出力:
// a
// b
// c
// d
// e
for-of 文の特徴
for-of 文で配列を反復する場合、変数を配列の要素の値で直接初期化できるため、典型的な反復の場合コードがシンプルになります。
配列の要素の値を一時変数を経由せずそのまま分割束縛することもできます。
const users = [
{ name: "Alice", age: 20 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 30 },
];
for (const { name, age } of users) {
console.log(name, age);
}
// 出力:
// Alice 20
// Bob 25
// Charlie 30
伝統的な for 文と違って反復の対象となる配列を繰り返し参照する必要がなく、of
の直後に 1 回だけ配列を書くこともできます。
for (const element of ["a", "b", "c"]) {
console.log(element);
}
// 出力:
// a
// b
// c
インデックスが必要な場合は、配列の entries メソッドを使って要素のインデックスと値のペアを反復することができます。
const array = ["a", "b", "c"];
for (const [i, element] of array.entries()) {
console.log(i, element);
}
// 出力:
// 0 a
// 1 b
// 2 c
配列の要素の値を順に(破壊的に)書き換える場合は、インデックスを取得して配列のその位置に代入する必要があります。このような場合は伝統的な for 文を使うほうが適しているかもしれません。
const array = ["a", "b", "c"];
for (let element of array) {
// この代入は変数 element のみを変更し、配列 array の内容には影響しない!
element = "../" + element + ".png";
}
console.log(array);
// 出力: [ 'a', 'b', 'c' ]
// インデックスを取得して代入する
for (const [index, element] of array.entries()) {
array[index] = "../" + element + ".png";
}
console.log(array);
// 出力: [ '../a.png', '../b.png', '../c.png' ]
伝統的な for 文を使う場合:
for (let i = 0; i < array.length; i++) {
array[i] = "../" + array[i] + ".png";
}
このようなケースの場合は、もとの配列を書き換えずに別の配列を新しく作る方針を考えたほうがよいことがあります。その方針では、配列の map メソッドを使うと最も簡潔にかけます:
const newArray = array.map((element) => "../" + element + ".png");
console.log(newArray);
// 出力: [ '../a.png', '../b.png', '../c.png' ]
// もとの配列は変更されていない
console.log(array);
// 出力: [ 'a', 'b', 'c' ]
疎な配列の扱い
for-of 文では疎な配列のプロパティが存在しない(空の)インデックスについても反復されます。
const array = ["a", , "c"];
for (const element of array) {
console.log(element);
}
// 出力:
// a
// undefined
// c
配列以外のオブジェクトに対する反復
配列に限らず、反復可能 (iterable) なオブジェクトに対しても for-of 文を使って反復処理を行うことができます。(配列は反復可能なオブジェクトの一種です。)
非同期反復可能なオブジェクトに対しては for-await-of 文を使って反復することができます。(詳細はこの記事では割愛します)
反復可能でないオブジェクトは for-of 文で反復できません。ただし、配列風オブジェクトであれば、Array.from メソッドなどを使って配列(や反復可能なオブジェクト)に変換してから反復処理を行うことができます。
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
// エラーになる例
for (const element of arrayLike) { // TypeError: arrayLike is not iterable
console.log(element);
}
for (const element of Array.from(arrayLike)) {
console.log(element);
}
// 出力:
// a
// b
// c
for-of 文の書き換え
array
が配列のとき、
for (const <変数> of array)
<文>
は
for (let i = 0; i < array.length; i++) {
const <変数> = array[i];
<文>
}
とだいたい同じ意味になります。(i
は <文>
で参照されない変数名 かつ <変数>
と異なる名前とする)
<文>
に break 文や continue 文や return 文や yield 演算子や await
や var
や関数宣言が含まれない場合、
array.forEach((<変数>) => {
<文>
});
とも書き換えることができます。
for-in 文
オブジェクトのプロパティを反復するための for 文です。
for (const <変数> in <反復対象>)
<文>
in より前の部分について
for-of 文と同様、const
の代わりに let
や var
を使って変数を宣言することもできます。変数を宣言せずに代入だけを行うことができるのも同様です。
for-in 文は、オブジェクトの列挙可能な (enumerable) プロパティを反復します。
JavaScript では配列はオブジェクトの一種であり、それぞれの要素についてインデックスの整数を文字列化したものをキー、要素の値を値とする列挙可能なデータプロパティを持っています。
また、配列には要素数を保持する length
というデータプロパティがありますが、このプロパティは列挙可能ではありません。
そのため、for-in 文を使って配列を反復することが(一応)可能です。
典型的な配列の反復は、次のような形に(一応)なります。
const array = ["a", "b", "c", "d", "e"];
for (const key in array) {
console.log(array[key]);
}
// 出力:
// a
// b
// c
// d
// e
列挙可能なプロパティは、反復対象となるオブジェクトのプロトタイプチェーンを遡って列挙されます。したがって、配列のプロトタイプチェーンにある Array.prototype
や Object.prototype
に列挙可能なプロパティが追加されていると、それらのプロパティも反復されてしまいます。
for-in 文の特徴
for-in 文は、配列の反復のための構文ではなく、オブジェクトのプロパティを反復するための構文です。配列の反復には代わりに for-of 文を使うことが推奨されます。
また、オブジェクトのプロパティを反復する場合も、プロトタイプチェーンを遡って列挙される動作が望ましくないことがしばしばあり、Object.keys
Object.values
Object.entries
などのメソッドを使って列挙可能なプロパティを配列として取得して、for-of 文を使って反復することがあります。
列挙順について、通常の使用範囲では配列のインデックスについては昇順で反復されると考えてよいでしょう。ただし、ループの途中で配列の要素が追加される場合、追加された要素については反復されない可能性があります。
疎な配列の扱い
for-in 文ではプロパティに対する反復を行うため、疎な配列の中でプロパティが存在しない(空の)インデックスについては反復されません。
3 種類の for 文に共通する特徴
-
<文>
の部分に 2 つ以上の文や宣言を書くためには、ブロック文 ({ ... }
) を使って、1 つの文にまとめる必要があります。
for (...) { console.log("Hello"); console.log("World"); }
- 1 つの文だけを書く場合にもブロック文を使うことができます。
for (...) { console.log("Hello"); }
- 1 つの文だけを書く場合にもブロック文を使うことができます。
- for 文自体は 1 つの文 (statement) なので、文が期待される場所に書くことができます。
- for 文を入れ子にすることができます。
for (...) for (...) ...;
- 関数の波括弧の中に for 文を書くことができます。
function foo() { for (...) ...; }
-
() => { for (...) ...; }
注意:() => for (...) ...;
のように書くことはできません。アロー関数式の() =>
の後ろに{
が続かない場合、=>
の右側は式が期待される(文が期待されない)ためです。
- for 文を入れ子にすることができます。
-
<文>
の内部で break 文を使うと、ループ全体の実行を終了できます。- break 文が実行されると、現在のイテレーションが直ちに終了し、そのループにおけるそれ以降のイテレーションは行われません。
- break 文が実行されると、終了させられた for 文の直後に実行が移ります。
-
<文>
の内部で continue 文を使うと、現在のイテレーションだけを終了できます。- continue 文が実行されると、現在のイテレーションは直ちに終了し、次のイテレーションの準備に移ります。
- continue 文が実行されると、for 文の
<文>
部分の直後に実行が移ります。- 伝統的な for 文の場合、
<更新>
の式の評価に移ります。
- 伝統的な for 文の場合、
- 各イテレーションでの処理対象を絞り込む際にネストを深くしないために使うことがあります。
for (const article of articles) { if (article.isDraft) continue; if (article.lastUpdated < oneWeekAgo) continue; if (article.likes < 10) continue; console.log(article); // 以下のようにどんどんネストが深くならない // if (!article.isDraft) // if (article.lastUpdated >= oneWeekAgo) // if (article.likes >= 10) // console.log(article); }
-
<文>
の内部で return 文を使うと、for 文を含んでいる関数全体の実行を終了できます。- return 文が実行されると、現在のイテレーションは直ちに終了し、そのループにおけるそれ以降のイテレーションは行われません。
- return 文が実行されると、関数全体の実行が終了し、関数の呼び出し元に実行が戻ります。
- for 文を含め、文にはラベルをつけることができます。
- ラベルは、文の直前に書かれ、ラベル名とコロン (
:
) で構成されます。 - ラベルを使って、break 文や continue 文でどの文を対象とするかを指定することができます。continue 文で for 文、while 文、do-while 文でない文のラベルを指定すると構文エラーになります。
outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (i === 1 && j === 1) { break outer; } console.log(i, j); } }
- 同じ名前のラベルを入れ子にすると構文エラーになります。
a: for (;;) { a: for (;;) { // 構文エラー // ... } } b: for (;;) { // ... } b: for (;;) { // 入れ子でないので構文エラーにならない // ... }
- ラベルを使わない break 文は、最も内側の for 文、while 文、do-while 文、switch 文を対象とします。
ラベルを使わない continue 文は、最も内側の for 文、while 文、do-while 文を対象とします。
そのような文が存在しない場合は構文エラーになります。
- ラベルは、文の直前に書かれ、ラベル名とコロン (
while 文, do-while 文
while 文と do-while 文は、処理を繰り返し実行する基本的な構文の 1 つです。
while 文
while (<条件>)
<文>
while 文が実行されるとき、次のような順序で処理が行われます。
-
<条件>
の式が評価されます。- 評価結果が falsy ならば、ループが終了し、実行が while 文の直後に移ります。
-
<文>
が実行されます。 - 1 に戻ります。
典型的な配列の反復は、(無理やり書くと)次のような形になります。
const array = ["a", "b", "c", "d", "e"];
let i = 0;
while (i < array.length) {
console.log(array[i]);
i++;
}
// 出力:
// a
// b
// c
// d
// e
do-while 文
do
<文>
while (<条件>);
do-while 文が実行されるとき、次のような順序で処理が行われます。
-
<文>
が実行されます。 -
<条件>
の式が評価されます。- 評価結果が falsy ならば、ループが終了し、実行が do-while 文の直後に移ります。
- 1 に戻ります。
do-while 文は、最初の 1 回は条件の評価をせずに必ず 1 回は <文>
が実行される点が while 文と異なります。
while 文, do-while 文の特徴
while 文や do-while 文は、配列の反復処理というよりも、条件によって繰り返し処理を行うことに重きを置いた構文です。
構文的な特徴は for 文と似ている点が多いです。
-
<文>
に 2 つ以上の文を書くためには、ブロック文 ({ ... }
) を使って、1 つの文にまとめる必要があります。 - while 文や do-while 文は 1 つの文なので、文が期待される場所に書くことができます。
- continue 文や break 文や return 文については、for 文と同様の挙動をします。
- while 文や do-while 文にもラベルをつけることができます。
while 文, do-while 文の書き換え
while (<条件>)
<文>
は
for (; <条件>; )
<文>
と同じ意味になります。
do
<文>
while (<条件>);
は
while (true) {
<文>
if (!(<条件>)) break;
}
と同じ意味になります。
配列のメソッド
JavaScript には、配列を操作するための便利なメソッドが用意されています。その中には配列の要素に対する反復処理を行うためのメソッドがいくつかあります。
反復処理を行う配列のメソッドの多くは、コールバック関数を引数に取ります。コールバック関数は、配列の各要素に対して実行される関数であり、反復して行う処理の内容を記述します。
コールバック関数は、通常 3 つの引数を渡されます。
- 要素: 配列の要素の値
- インデックス: 要素のインデックス
- 配列: 反復対象の配列全体
forEach メソッド
配列の forEach メソッドは、配列の各要素に対して順にコールバック関数を実行します。すべての要素に対して実行が終了すると、forEach メソッドの呼び出しが終了し undefined を返します。
const array = ["a", "b", "c"];
array.forEach((element, i, obj) => {
console.log(element, i, obj);
});
// 出力:
// a 0 [ 'a', 'b', 'c' ]
// b 1 [ 'a', 'b', 'c' ]
// c 2 [ 'a', 'b', 'c' ]
コールバック関数の返り値は無視されます。そのため、アロー関数式の中身が式文であれば、波括弧を省略して =>
のあとに直接その式を書いても同じ挙動になります。
const array = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
const result = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
array.forEach((element) => { result[element] += 1; });
// 以下でも同じ挙動
array.forEach((element) => result[element] += 1);
forEach メソッドの特徴
配列の forEach メソッドは反復処理について意味のある結果を返さない(常に undefined を返す)ため、反復処理が意味を持つためにはコールバック関数の中で副作用を持つ処理を行う必要があります。
上記の例では、console.log
による出力や result
オブジェクトのプロパティの変更が副作用にあたります。
コールバック関数の中で、ループ全体を途中で終了し、ループの直後に処理を移す(for 文での break 文や return 文に相当する)手段はありません。(エラーを送出することで forEach メソッドの実行を終了させることはできますが、forEach メソッドを呼び出す文を try-catch 文で囲んでエラーを捕捉する必要があります)
現在のイテレーションを途中で終了し、次のイテレーションへ進む(for 文での continue 文に相当する)には return 文を使います。
このような return 文は各イテレーションでの処理を絞り込む際にネストを深くしないために使うことがあります。(このテクニックは早期リターンと呼ばれ、反復処理に限らず一般の関数でも使われます)
articles.forEach((article) => {
if (article.isDraft) return;
if (article.lastUpdated < oneWeekAgo) return;
if (article.likes < 10) return;
console.log(article);
// 以下のようにどんどんネストが深くならない
// if (!article.isDraft)
// if (article.lastUpdated >= oneWeekAgo)
// if (article.likes >= 10)
// console.log(article);
});
引数の変数に代入することは、もとの配列の要素に影響しません。
const array = [1, 2, 3, 4, 5];
array.forEach((element) => {
// この代入はコールバック関数内の変数 element のみを変更し、
// 反復対象の配列 array の内容には影響しない!
element = element * 2;
});
console.log(array);
// 出力: [ 1, 2, 3, 4, 5 ]
引数を分割束縛することができます。
const users = [
{ name: "Alice", age: 20 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 30 },
];
users.forEach(({ name, age }) => {
console.log(name, age);
});
forEach メソッドで反復処理を行う場合、非同期処理を逐次実行することはできません。非同期処理を逐次実行するには、for-of 文と await 演算子を使って反復処理を行うか、Promise のメソッドを使う必要があります。
const result = [];
const registerElement = async (element) => {
// 1 秒かかる非同期処理を再現
await new Promise((resolve) => setTimeout(resolve, 1000));
result.push(element);
};
const array = ["a", "b", "c", "d", "e"];
// 前の registerElement の処理が終わる前に次の registerElement の処理が始まってしまう!
array.forEach(async (element) => {
await registerElement(element);
});
// また、5 回の registerElement の処理はこの時点でどれも終わっていない!
console.log(result); // 出力: []
代わりに:
// 略
const array = ["a", "b", "c", "d", "e"];
for (const element of array) {
await registerElement(element);
// この時点でこの registerElement の処理は終わっている
}
console.log(result); // 出力: [ 'a', 'b', 'c', 'd', 'e' ]
// 略
const array = ["a", "b", "c", "d", "e"];
let promise = Promise.resolve();
array.forEach((element) => {
// 前の registerElement の処理が終わったら次の registerElement の処理が始まるようにする
promise = promise.then(async () => {
await registerElement(element);
});
});
await promise; // 最後の registerElement の処理が終わるまで待つ
console.log(result); // 出力: [ 'a', 'b', 'c', 'd', 'e' ]
// 略
const array = ["a", "b", "c", "d", "e"];
await array.reduce(
(prev, element) => prev.then(async () => {
await registerElement(element);
}),
Promise.resolve()
);
console.log(result); // 出力: [ 'a', 'b', 'c', 'd', 'e' ]
反復対象となるインデックス
forEach メソッドが反復する範囲は、メソッドが呼び出された時点での配列の長さをもとに決まります。そのため、forEach メソッドのコールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されません。
疎な配列のプロパティが存在しない(空の)インデックスについては反復されません。
const array = ["a", , "c"];
array.forEach((element, i) => {
console.log(i, element);
});
// 出力:
// 0 a
// 2 c
コールバック関数の第 3 引数
コールバック関数の第 3 引数は、メソッドチェーンの終端で forEach メソッドを呼び出す際に役立つことがあります。
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const twinPrimes = [];
array
.filter((element) => isPrime(element)) // 素数だけを取り出す
.forEach((element, i, primes) => {
// 第 3 引数 primes の値は、反復対象の配列、つまり素数だけに絞り込まれた配列(array ではなく array.filter(...))
if (i === 0) return; // 先頭は直前の素数がないのでスキップ
const prev = primes[i - 1]; // 直前の素数を primes から取得する
// 双子素数かどうかを判定
if (element - prev === 2) {
twinPrimes.push([prev, element]);
}
});
console.log(twinPrimes);
// 出力: [ [ 3, 5 ], [ 5, 7 ] ]
forEach メソッドの第 2 引数
forEach メソッドの第 2 引数で、コールバック関数の中で this
として使われる値を指定することができます。
class Logger {
constructor(prefix) {
this.prefix = prefix;
}
log(element) {
console.log(this.prefix, element);
}
}
const logger = new Logger("LOG:");
const array = ["a", "b", "c"];
array.forEach(logger.log); // TypeError: Cannot read properties of undefined (reading 'prefix')
array.forEach(logger.log, logger);
// 出力:
// LOG: a
// LOG: b
// LOG: c
// 以下とだいたい同じ意味になります
array.forEach(logger.log.bind(logger));
第 1 引数の値がアロー関数の場合は第 2 引数を指定することに意味はありません。なぜなら、アロー関数式の中の this
は常にアロー関数式のその外側のスコープの this
と一致するからです。
配列の forEach メソッドと同名のメソッドが他のオブジェクトにも存在することがあります。例えば、Map や Set にも forEach メソッドがあります。そのオブジェクトに含まれる要素に対して反復処理を行うという点で共通しており、コールバック関数の引数が配列のものと類似しています。
配列の forEach メソッドは、配列風オブジェクトに対しても反復処理を行うことができます。
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
Array.prototype.forEach.call(arrayLike, (element, i) => {
console.log(i, element);
});
// 出力:
// 0 a
// 1 b
// 2 c
forEach メソッドの書き換え
array
が配列で、<処理>
に return 文や var
や関数宣言が含まれない場合、
array.forEach((element) => {
<処理>
});
は
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
let element = array[i];
<処理>
}
とだいたい同じ意味になります。(i
や length
は <処理>
の中で参照されない変数名とする)
また、array
が密な配列であれば
for (let element of array) {
<処理>
}
ともだいたい同じ意味になります(forEach メソッドの実行中に array
の長さが増加したときに、追加されたインデックスについても反復されるようになるという違いがあります)。
<処理>
に return 文が含まれる場合は、この for 文に対する continue 文に置き換えることになります。
map メソッド
配列の map メソッドは、配列の各要素に対して順にコールバック関数を実行し、その返り値を集めます。すべての要素に対して実行が終了すると、map メソッドはその返り値を集めた新しい配列を返します。もとの配列は変更されません。
const array = [1, 2, 3, 4, 5];
// 各要素の値を 2 倍した新しい配列を作成
const doubled = array.map((element) => element * 2);
console.log(doubled); // 出力: [ 2, 4, 6, 8, 10 ]
// もとの配列は変更されていない
console.log(array); // 出力: [ 1, 2, 3, 4, 5 ]
新しい配列の長さは、もとの配列の(最初の)長さと同じになります。
コールバック関数の返り値は、新しい配列の対応するインデックスの要素の値になります。そのため、forEach メソッドの場合と異なり返り値の有無で意味が変わります。
const array = [1, 2, 3];
const doubled = array.map((element) => element * 2);
console.log(doubled); // 出力: [ 2, 4, 6 ]
const array2 = array.map((element) => { element * 2; });
console.log(array2); // 出力: [ undefined, undefined, undefined ]
注意: =>
のあとに {
が続くアロー関数式では、関数が値を返すためには return 文を使う必要があります。return 文を使わずに関数の中身の実行が(ブロックの末尾に到達することで)終わると、その関数の返り値は暗黙的に undefined になります。
map メソッドの特徴
配列の map メソッドは、コールバック関数を 1 つの写像とみなして配列の各要素の値をその写像で写す (map) ことに主眼を置いたメソッドです。
そのため、一般的にはコールバック関数は返り値を持ち、map メソッドの返り値となる配列を使って後続の処理を行うことが多いです。
配列の map メソッドの結果もまた配列になるため、map メソッドを呼び出す式の後ろにさらに map メソッドや他の配列のメソッドをつなげることができます。このようなメソッドをつなげる形をメソッドチェーンと呼ぶことがあります。
const array = [1, 2, 3, 4, 5];
const doubledPlusOne = array
.map((element) => element * 2) // 各要素の値を 2 倍して
.map((element) => element + 1); // さらに 1 を足す
console.log(doubledPlusOne); // 出力: [ 3, 5, 7, 9, 11 ]
array
.map((element) => element * 3) // 各要素の値を 3 倍して
.filter((element) => element % 2 === 0) // 2 で割り切れるものに絞り込んで
.forEach((element) => console.log(element)); // それぞれを出力
// 出力:
// 6
// 12
コールバック関数の中で、ループ全体を途中で終了し、ループの直後に処理を移す(for 文での break 文や return 文に相当する)手段が無い点は forEach メソッドと同様です。
現在のイテレーションを途中で終了し、次のイテレーションへ進む(for 文での continue 文に相当する)には return 文を使うという点も forEach メソッドと同様ですが、その return 文で返す値は map メソッドの返り値となる配列の要素の値となります。map メソッドだけでは結果の配列から要素を取り除くことはできません。
const array = [1, 2, 3, 4, 5];
const newArray = array.map((element) => {
if (element % 2 === 0) return; // 偶数は無視したい
return element * element;
});
// 結果の配列に undefined が含まれる
console.log(newArray); // 出力: [ 1, undefined, 9, undefined, 25 ]
要素を取り除きたい場合は、さらに filter メソッドを使って不要な要素を除外することがあります。
const newArray = array
.map((element) => {
if (element % 2 === 0) return; // 偶数は無視したい
return element * element;
})
.filter((element) => element !== undefined);
console.log(newArray); // 出力: [ 1, 9, 25 ]
map メソッドの代わりに flatMap メソッドを使うこともできます。
const newArray = array.flatMap((element) => {
if (element % 2 === 0) return []; // 偶数は無視したい
return [element * element];
});
console.log(newArray); // 出力: [ 1, 9, 25 ]
map メソッドに渡すコールバック関数を非同期関数にすると、map メソッドの返り値は Promise からなる配列になります。
const getDoubled = async (element) => {
// 1 秒かかる非同期処理を再現
await new Promise((resolve) => setTimeout(resolve, 1000));
return element * 2;
};
const array = [1, 2, 3];
const promises = array.map(async (element) => {
const doubled = await getDoubled(element);
return doubled;
});
console.log(promises);
// 出力: [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
Promise.all
を使うと「Promise からなる配列」を「「各 Promise の解決値からなる配列」で解決される Promise」に変換できます。これと await 演算子を組み合わせることで、非同期関数で各要素を処理した新しい配列を取得できます。
// 略
const array = [1, 2, 3];
const results = await Promise.all(
array.map(async (element) => {
const doubled = await getDoubled(element);
return doubled;
})
);
console.log(results); // 出力: [ 2, 4, 6 ]
このとき各要素の非同期処理は並列で実行されることに注意してください。全体の処理時間は最も時間のかかる非同期処理によって決まります。(上の例では、各要素の非同期処理がおよそ 1 秒かかるため、全体の処理時間もおよそ 1 秒かかります)
非同期処理を逐次実行するには、map メソッドの代わりに for-of 文と await 演算子を使って反復処理を行うか、Promise のメソッドを使う必要があります。
// 略
const array = [1, 2, 3];
const results = [];
for (const element of array) {
const doubled = await getDoubled(element);
results.push(doubled);
}
console.log(results); // 出力: [ 2, 4, 6 ]
// 略
const array = [1, 2, 3];
const results = await array.reduce(
(prev, element) => prev.then(async (results) => {
const doubled = await getDoubled(element);
results.push(doubled);
return results;
}),
Promise.resolve([])
);
console.log(results); // 出力: [ 2, 4, 6 ]
逐次処理の場合、全体の処理時間は各要素の非同期処理の合計時間になります。(上の例では、各要素の非同期処理がそれぞれおよそ 1 秒かかるため、全体の処理時間はおよそ 3 秒かかります)
反復対象となるインデックス
map メソッドが反復する範囲と返り値となる新しい配列の長さは、メソッドが呼び出された時点でのもとの配列の長さをもとに決まります。そのため、map メソッドのコールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されず、新しい配列にも含まれません。
疎な配列のプロパティが存在しない(空の)インデックスについては反復されず、新しい配列でも存在しない(空の)インデックスになります。
const array = ["a", , "c"];
const newArray = array.map((element, i) => element.repeat(i + 1));
console.log(newArray); // 出力: [ 'a', <1 empty item>, 'ccc' ]
コールバック関数の第 3 引数は、メソッドチェーンで map メソッドを呼び出す際に役立つことがあります。(forEach と同様)
コールバック関数には常に 3 つの引数が渡されるため、省略可能な引数を受け取るような関数の取り扱いに注意が必要です。
特に、parseInt 関数をそのままコールバック関数として使うと、意図しない結果になることがあります。
const array = ["10", "10", "10", "10"];
// 10 進数の整数として解釈した数値の配列に変換したいが……
const newArray = array.map(parseInt);
console.log(newArray); // 出力: [ 10, NaN, 2, 3 ]
// parseInt 関数は省略可能な第 2 引数が指定された場合、その値を基数として第 1 引数の文字列を解釈します。
// 一方で map メソッドはコールバック関数の第 2 引数にインデックスを渡します。
parseInt("10", 0, array) // → 10 (自動で 10 進数として解釈される)
parseInt("10", 1, array) // → NaN (1 は基数として無効)
parseInt("10", 2, array) // → 2 (2 進数として解釈される)
parseInt("10", 3, array) // → 3 (3 進数として解釈される)
// 意図通りに動作する例: 第 2 引数以降を parseInt に渡さない
const newArray2 = array.map((element) => parseInt(element));
console.log(newArray2); // 出力: [ 10, 10, 10, 10 ]
map メソッドの第 2 引数で、コールバック関数の中で this
として使われる値を指定することができます。(forEach と同様)
配列の map メソッドは、配列風オブジェクトに対しても呼び出すことができます。返り値は配列になります。
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
const newArray = Array.prototype.map.call(arrayLike, (element, i) => element.repeat(i + 1));
console.log(newArray); // 出力: [ 'a', 'bb', 'ccc' ]
map メソッドの書き換え
array
が配列のとき、
const newArray = array.map((element) => {
const newElement = <式>;
return newElement;
});
は <式>
が array
の値を変更しないならば
const newArray = new Array(+array.length);
for (let i = 0; i < newArray.length; i++) {
if (!(i in array)) continue;
let element = array[i];
const newElement = <式>;
newArray[i] = newElement;
}
とだいたい同じ意味になります。(i
は <式>
の中で参照されない変数名とする)
また、array
が密な配列のとき、
const newArray = [];
for (let element of array) {
const newElement = <式>;
newArray.push(newElement);
}
ともだいたい同じ意味になります(map メソッドの実行中に array
の長さが増加したときに、追加されたインデックスについても反復されるようになるという違いがあります)。
map メソッドを複数つなげるパターンの書き換え
array
が配列のとき、
const newArray = array
.map((element) => <element を使う式 1>)
.map((element) => <element を使う式 2>)
.map((element) => <element を使う式 3>);
は <element を使う式 1>
<element を使う式 2>
<element を使う式 3>
が副作用を持たないとき
const newArray = array.map((element) => {
const newElement1 = <element を使う式 1>;
const newElement2 = <newElement1 を使う式 2>;
const newElement3 = <newElement2 を使う式 3>;
return newElement3;
});
と書き換えられます。(newElement1
newElement2
newElement3
は、3 つの式の中で参照されない変数名とする)
map メソッドはすべての要素に対して順にコールバック関数を実行したあと配列を返すため、map メソッドを複数つなげた場合、
- すべての要素に対して順に式 1 が評価される
- すべての要素に対して順に式 2 が評価される
- すべての要素に対して順に式 3 が評価される
という順番で処理が行われます。一方で 1 つの map メソッドの中で 3 つの式を実行する場合、
- 1 番目の要素に対して式 1、式 2、式 3 が順に評価される
- 2 番目の要素に対して式 1、式 2、式 3 が順に評価される
- …
- 最後の要素に対して式 1、式 2、式 3 が順に評価される
という順番で処理が行われます。そのため、副作用を持つ式を使う場合は、この書き換えは意味を変えることがあります。
const array = ["a", "b", "c"];
const newArray = array
.map((element) => {
// 副作用: console.log による出力
console.log("1:", element);
return element.toUpperCase();
})
.map((element) => {
// 副作用: console.log による出力
console.log("2:", element);
return element.repeat(2);
});
console.log(newArray);
// 出力:
// 1: a
// 1: b
// 1: c
// 2: A
// 2: B
// 2: C
// [ 'AA', 'BB', 'CC' ]
const array = ["a", "b", "c"];
const newArray = array.map((element) => {
console.log("1:", element);
const newElement1 = element.toUpperCase();
console.log("2:", newElement1);
const newElement2 = newElement1.repeat(2);
return newElement2;
});
console.log(newArray);
// 出力: (最後の配列は同じだが、途中の出力の順番が異なる)
// 1: a
// 2: A
// 1: b
// 2: B
// 1: c
// 2: C
// [ 'AA', 'BB', 'CC' ]
map メソッドのあとに flat メソッドを使う場合
array
が配列のとき、
const newArray = array
.map((element) => <式>)
.flat();
は(.flat(1);
の場合も含む)
const newArray = array.flatMap((element) => <式>);
とだいたい同じ意味になります。
map メソッドの返り値を使わない場合
配列の map メソッドの返り値を使わない場合は代わりに forEach メソッドを使うことができます。
array.map((element) => {
<処理>
});
は
array.forEach((element) => {
<処理>
});
とだいたい同じ意味になります。
filter メソッド
配列の filter メソッドは、配列の各要素に対して順にコールバック関数を実行し、その返り値が truthy だったインデックスの要素の値だけを集めた新しい密な配列を返します。もとの配列は変更されません。
const array = [1, 2, 3, 4, 5];
// 奇数だけを取り出した新しい配列を作成
const odd = array.filter((element) => element % 2 === 1);
console.log(odd); // 出力: [ 1, 3, 5 ]
// もとの配列は変更されていない
console.log(array); // 出力: [ 1, 2, 3, 4, 5 ]
新しい配列の長さは、もとの配列の(最初の)長さ以下になります。とくに、もとの配列が空配列のとき、新しい配列はつねに空配列になります。
filter メソッドの特徴
配列の filter メソッドは、特定の条件を満たす要素の値だけを取り出す操作を行います。その条件はコールバック関数を使って自由に指定することができます。
配列の filter メソッドの結果もまた配列になるため、filter メソッドを呼び出す式の後ろにさらに filter メソッドや他の配列のメソッドをつなげることができます。このようなメソッドをつなげる形をメソッドチェーンと呼ぶことがあります。
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
const newArray = array
.filter((element) => element % 3 === 0) // 3 で割り切れるものに絞り込んで
.filter((element) => element % 5 === 0); // さらに 5 で割り切れるものに絞り込む
console.log(newArray); // 出力: [ 15 ]
array
.filter((element) => element % 3 === 0) // 3 で割り切れるものに絞り込んで
.map((element) => element % 5) // 5 で割った余りを求めて
.forEach((element) => console.log(element)); // それぞれを出力
// 出力:
// 3
// 1
// 4
// 2
// 0
コールバック関数の中で、ループ全体を途中で終了し、ループの直後に処理を移す(for 文での break 文や return 文に相当する)手段が無い点は forEach メソッドと同様です。
現在のイテレーションを途中で終了し、次のイテレーションへ進む(for 文での continue 文に相当する)には return 文を使います。この return 文で返す値が falsy であれば、その要素の値は新しい配列に含まれません。
コールバック関数が返した値は暗黙的に真理値 (Boolean) に変換されます。そのため、コールバック関数を恒等関数とすることで配列の truthy な要素の値だけを取り出すことができます。
const array = [false, 0, "", null, undefined, NaN, 1, "a", {}, []];
const truthy = array.filter((element) => element);
console.log(truthy); // 出力: [ 1, 'a', {}, [] ]
これと同じ意味で Boolean
を真理値に変換する関数として使うこともあります。(Boolean
は 2 つ目以降の引数を無視します)
const truthy = array.filter(Boolean);
console.log(truthy); // 出力: [ 1, 'a', {}, [] ]
絞り込むだけでなく要素の値を変換したい場合は、map メソッドと組み合わせたり、flatMap メソッドを使うことがあります。
const words = ["I", "have", "42", "apples", "and", "3", "bananas"];
const numbers = words
.filter((word) => /^\d+$/.test(word)) // 数字だけからなるものに絞り込んで
.map((word) => Number(word)) // 数値に変換して
.filter((number) => number >= 20); // 20 以上に絞り込む
console.log(numbers); // 出力: [ 42 ]
const words = ["I", "have", "42", "apples", "and", "3", "bananas"];
const numbers = words
.flatMap((word) => {
if (!/^\d+$/.test(word)) return []; // 数字以外を含むものを取り除く
const number = Number(word); // 数値に変換
if (number < 20) return []; // 20 未満を取り除く
return [number];
});
console.log(numbers); // 出力: [ 42 ]
反復対象となるインデックス
filter メソッドが反復する範囲は、メソッドが呼び出された時点での配列の長さをもとに決まります。そのため、filter メソッドのコールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されず、新しい配列にも含まれません。
疎な配列のプロパティが存在しない(空の)インデックスについては反復されず、新しい配列ではインデックスが詰められます。(密な配列になります)
const array = ["a", , "c"];
const newArray = array.filter(() => true); // すべての要素が条件を満たす
console.log(newArray); // 出力: [ 'a', 'c' ]
非同期処理を含むコールバック関数
filter メソッドに渡すコールバック関数で非同期に真理値を返すことはできません。
/** 奇数かどうかを返す非同期関数 */
const isOdd = async (num) => {
// 1 秒かかる非同期処理を再現
await new Promise((resolve) => setTimeout(resolve, 1000));
return num % 2 === 1;
};
const array = [1, 2, 3];
const odds = array.filter(async (element) => {
const result = await isOdd(element);
return result;
});
console.log(odds);
// 出力: [ 1, 2, 3 ]
これは、非同期関数の返り値が Promise オブジェクトになるためです。Promise オブジェクトは truthy な値であるため、filter メソッドが返す新しい配列にはもとの配列のすべての要素が含まれてしまいます。
代わりに、for-of 文と await 演算子を使った逐次の反復処理を行うか、map メソッドと Promise.all
を併用して並列処理を行うことができます。
// 略
const array = [1, 2, 3];
const odds = [];
// 逐次処理を行う
for (const element of array) {
const result = await isOdd(element);
if (result) {
odds.push(element);
}
}
console.log(odds); // 出力: [ 1, 3 ]
// 略
const array = [1, 2, 3];
const odds = (
// map メソッドで非同期処理を並列実行し、Promise.all ですべての結果を待つ
await Promise.all(
array.map(async (element) => {
const result = await isOdd(element);
// もとの要素の値 element と非同期処理で得た真理値 result をプロパティに持つ一時的なオブジェクトを作成
return { element, result };
})
)
)
.filter(({ result }) => result) // 真理値を見て絞り込む
.map(({ element }) => element); // 各要素(一時的なオブジェクト)をもとの要素の値に戻す
console.log(odds); // 出力: [ 1, 3 ]
コールバック関数の第 3 引数は、メソッドチェーンで filter メソッドを呼び出す際に役立つことがあります。(forEach と同様)
filter メソッドの第 2 引数で、コールバック関数の中で this
として使われる値を指定することができます。(forEach と同様)
配列の filter メソッドは、配列風オブジェクトに対しても呼び出すことができます。返り値は配列になります。
filter メソッドの書き換え
array
が配列のとき、
const newArray = array.filter((element) => {
const condition = <条件>;
return condition;
});
は <条件>
が array
の値を変更しないならば
const newArray = [];
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
let element = array[i];
const condition = <条件>;
if (condition) {
newArray.push(element);
}
}
とだいたい同じ意味になります。(i
length
は <条件>
の中で参照されない変数名とする)
また、array
が密な配列のとき、
const newArray = [];
for (let element of array) {
const condition = <条件>;
if (condition) {
newArray.push(element);
}
}
ともだいたい同じ意味になります(filter メソッドの実行中に array
の長さが増加したときに、追加されたインデックスについても反復されるようになるという違いがあります)。
some メソッド
配列の some メソッドは、配列の各要素に対して順にコールバック関数を実行し、その返り値が truthy だったらそこで反復を終了し、true を返します。truthy な返り値が返ることなく反復が終了した場合は false を返します。
const array = [1, 2, 3, 4, 5];
const hasEven = array.some((element) => {
console.log("is even?", element);
return element % 2 === 0;
});
console.log(hasEven);
// 出力:
// is even? 1
// is even? 2
// true
とくに、もとの配列が空配列のとき、コールバック関数は呼ばれることなく、some メソッドは常に false を返します。
some メソッドの特徴
配列の some メソッドは、特定の条件を満たす要素が存在するかどうかを調べる操作を行います。その条件はコールバック関数を使って自由に指定することができます。
これは記号論理学での存在量化子「∃」に相当する操作です。全称量化子に相当する every メソッドと対になるメソッドです。
some メソッドが true を返した場合、もとの配列のいずれかの要素が条件を満たしたことを意味します。
some メソッドが false を返した場合、もとの配列のすべての要素が条件を満たさなかったことを意味します。(もとの配列が空配列だった場合も含みます)
配列の some メソッドの結果は真理値 (Boolean) になります。そのため、if 文などの分岐条件に使うことができます。
if (array.some((element) => element % 2 === 0)) {
console.log("array には偶数が含まれています");
} else {
console.log("array には偶数が含まれていません");
}
また、別の some メソッドに渡すコールバック関数の返り値として使うこともできます。
const groups = [
["for", "for-of", "for-in"],
["while", "do-while"],
["forEach", "map", "filter", "reduce"],
];
// "c" を含む文字列が 2 次元配列 groups の中に存在するかどうか
const matches = groups.some(
(group) => group.some((method) => method.includes("c"))
);
console.log(matches); // 出力: true
コールバック関数が truthy な値を返すと、その時点で反復が終了し、true が返されます。この動作は for 文での break 文または return 文に相当します。これにより、長い配列であっても、先頭付近に条件を満たす要素があれば配列の残りの要素についての反復がされないため、効率よく判定が可能です。
現在のイテレーションを途中で終了し、次のイテレーションへ進む(for 文での continue 文に相当する)には return 文で falsy な値を返します。
some メソッドの返り値は条件を満たす要素が存在したかどうかを表す真理値になります。
どのインデックスの要素が条件を満たしたかを知りたい場合は、代わりに findIndex メソッドを使うことができます。条件を満たした要素の値自体を取り出したい場合は、find メソッドを使うことができます。
反復対象となるインデックス
some メソッドが反復しうる範囲は、メソッドが呼び出された時点での配列の長さをもとに決まります。そのため、some メソッドのコールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されません。
疎な配列のプロパティが存在しない(空の)インデックスについては反復されません。
some メソッドに渡すコールバック関数で非同期に真理値を返すことはできません。理由は filter メソッドと同様です。代わりに for-of 文と await 演算子を使った逐次の反復処理を行うことができます。
コールバック関数の第 3 引数は、メソッドチェーンで some メソッドを呼び出す際に役立つことがあります。(forEach と同様)
some メソッドの第 2 引数で、コールバック関数の中で this
として使われる値を指定することができます。(forEach と同様)
配列の some メソッドは、配列風オブジェクトに対しても呼び出すことができます。
some メソッドの書き換え
array
が配列のとき、
let result = array.some((element) => {
const condition = <条件>;
return condition;
});
は <条件>
が array
の値を変更しないならば
let result = false;
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
let element = array[i];
const condition = <条件>;
if (condition) {
result = true;
break;
}
}
とだいたい同じ意味になります。(i
length
は <条件>
の中で参照されない変数名とする)
また、array
が密な配列のとき、
let result = false;
for (let element of array) {
const condition = <条件>;
if (condition) {
result = true;
break;
}
}
ともだいたい同じ意味になります(some メソッドの実行中に array
の長さが増加したときに、追加されたインデックスについても反復されるようになるという違いがあります)。
||
演算子を使って、たとえば次のような書き換えができます。
[1, 2, 3].some((element) => f(element))
// 上と下は同じ結果になる
Boolean(f(1) || f(2) || f(3))
ド・モルガンの法則から、次のような書き換えができます。
array.some((element) => !(<条件>))
// 上と下は同じ結果になる
!array.every((element) => <条件>)
!array.some((element) => <条件>)
// 上と下は同じ結果になる
array.every((element) => !(<条件>))
特定の値をもつ要素があるかを調べるパターンの書き換え
配列 array
が特定の値 search
を要素の値として持つかどうかを調べる場合、
const search = <値>;
const hasValue = array.some((element) => element === search);
は
const search = <値>;
const hasValue = array.indexOf(search) !== -1;
// または
const hasValue = array.indexOf(<値>) !== -1;
とだいたい同じ意味になります。
search
の値が NaN でなければ
const search = <値>;
const hasValue = array.includes(search);
// または
const hasValue = array.includes(<値>);
ともだいたい同じ意味になります。(search
の値が NaN の場合は、some メソッドや indexOf メソッドのコード例では結果は常に false になり、includes メソッドのコード例では結果は array
に NaN が含まれるかどうかを示す真理値になります)
some メソッドを使う場合 <値>
が配列の各要素に対して繰り返し評価されるのを避けるために、上記のコード例のようにコールバック関数の外側で変数 search
に代入する場合がありますが、includes メソッドを使うと <値>
を直接引数に渡すこともできます。
every メソッド
配列の every メソッドは、配列の各要素に対して順にコールバック関数を実行し、その返り値が falsy だったらそこで反復を終了し、false を返します。falsy な返り値が返ることなく反復が終了した場合は true を返します。
const array = [1, 2, 3, 4, 5];
const allOdd = array.every((element) => {
console.log("is odd?", element);
return element % 2 === 1;
});
console.log(allOdd);
// 出力:
// is odd? 1
// is odd? 2
// false
とくに、もとの配列が空配列のとき、コールバック関数は呼ばれることなく、every メソッドは常に true を返します。
every メソッドの特徴
配列の every メソッドは、すべての要素が特定の条件を満たすかどうかを調べる操作を行います。その条件はコールバック関数を使って自由に指定することができます。
これは記号論理学での全称量化子「∀」に相当する操作です。存在量化子に相当する some メソッドと対になるメソッドです。
every メソッドが true を返した場合、もとの配列のすべての要素が条件を満たしたことを意味します。(もとの配列が空配列だった場合も含みます)
every メソッドが false を返した場合、もとの配列のいずれかの要素が条件を満たさなかったことを意味します。
配列の every メソッドの結果は真理値 (Boolean) になります。そのため、if 文などの分岐条件に使うことができます。
if (array.every((element) => element % 2 === 0)) {
console.log("array の要素の値はすべて偶数です");
} else {
console.log("array に偶数でない値の要素が含まれています");
}
また、別の every メソッドに渡すコールバック関数の返り値として使うこともできます。(some メソッドと同様)
コールバック関数が falsy な値を返すと、その時点で反復が終了し、false が返されます。この動作は for 文での break 文または return 文に相当します。これにより、長い配列であっても、先頭付近に条件を満たさない要素があれば配列の残りの要素についての反復がされないため、効率よく判定が可能です。
現在のイテレーションを途中で終了し、次のイテレーションへ進む(for 文での continue 文に相当する)には return 文で truthy な値を返します。
every メソッドの返り値は条件を満たす要素がすべて存在したかどうかを表す真理値になります。
どのインデックスの要素が条件を満たさなかったかを知りたい場合は、代わりに findIndex メソッドを使うことができます。条件を満たさなかった要素の値自体を取り出したい場合は、find メソッドを使うことができます。
反復対象となるインデックス
every メソッドが反復しうる範囲は、メソッドが呼び出された時点での配列の長さをもとに決まります。そのため、every メソッドのコールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されません。
疎な配列のプロパティが存在しない(空の)インデックスについては反復されません。
every メソッドに渡すコールバック関数で非同期に真理値を返すことはできません。理由は filter メソッドと同様です。代わりに for-of 文と await 演算子を使った逐次の反復処理を行うことができます。
コールバック関数の第 3 引数は、メソッドチェーンで every メソッドを呼び出す際に役立つことがあります。(forEach と同様)
every メソッドの第 2 引数で、コールバック関数の中で this
として使われる値を指定することができます。(forEach と同様)
配列の every メソッドは、配列風オブジェクトに対しても呼び出すことができます。
every メソッドの書き換え
array
が配列のとき、
let result = array.every((element) => {
const condition = <条件>;
return condition;
});
は <条件>
が array
の値を変更しないならば
let result = true;
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
let element = array[i];
const condition = <条件>;
if (!condition) {
result = false;
break;
}
}
とだいたい同じ意味になります。(i
length
は <条件>
の中で参照されない変数名とする)
また、array
が密な配列のとき、
let result = true;
for (let element of array) {
const condition = <条件>;
if (!condition) {
result = false;
break;
}
}
ともだいたい同じ意味になります(every メソッドの実行中に array
の長さが増加したときに、追加されたインデックスについても反復されるようになるという違いがあります)。
&&
演算子を使って、たとえば次のような書き換えができます。
[1, 2, 3].every((element) => f(element))
// 上と下は同じ結果になる
Boolean(f(1) && f(2) && f(3))
ド・モルガンの法則から、次のような書き換えができます。
array.every((element) => !(<条件>))
// 上と下は同じ結果になる
!array.some((element) => <条件>)
!array.every((element) => <条件>)
// 上と下は同じ結果になる
array.some((element) => !(<条件>))
find メソッド, findIndex メソッド, findLast メソッド, findLastIndex メソッド
配列の find メソッドは、配列の各要素に対して順にコールバック関数を実行し、その返り値が truthy だったらそこで反復を終了し、その要素の値を返します。truthy な返り値が返ることなく反復が終了した場合は undefined を返します。
const array = [1, 2, 3, 4, 5];
const firstEven = array.find((element) => {
console.log("is even?", element);
return element % 2 === 0;
});
console.log(firstEven);
// 出力:
// is even? 1
// is even? 2
// 2
const firstNegative = array.find((element) => {
console.log("is negative?", element);
return element < 0;
});
console.log(firstNegative);
// 出力:
// is negative? 1
// is negative? 2
// is negative? 3
// is negative? 4
// is negative? 5
// undefined
配列の findIndex メソッドは、find メソッドと似ていますが、コールバック関数が truthy な値を返した要素の値の代わりにその要素のインデックスを返します。truthy な返り値が返ることなく反復が終了した場合は −1 を返します。
const array = [1, 2, 3, 4, 5];
const firstEvenIndex = array.findIndex((element) => {
console.log("is even?", element);
return element % 2 === 0;
});
console.log(firstEvenIndex);
// 出力:
// is even? 1
// is even? 2
// 1
const firstNegativeIndex = array.findIndex((element) => {
console.log("is negative?", element);
return element < 0;
});
console.log(firstNegativeIndex);
// 出力:
// is negative? 1
// is negative? 2
// is negative? 3
// is negative? 4
// is negative? 5
// -1
とくに、もとの配列が空配列のとき、コールバック関数は呼ばれることなく、find メソッドは常に undefined を、findIndex メソッドは常に −1 を返します。
findLast メソッドと findLastIndex メソッドは、find メソッドと findIndex メソッドとそれぞれ似ていますが、配列の末尾から先頭に向かって反復する点が異なります。
const array = [1, 2, 3, 4, 5];
const lastEven = array.findLast((element) => {
console.log("is even?", element);
return element % 2 === 0;
});
console.log(lastEven);
// 出力:
// is even? 5
// is even? 4
// 4
const lastNegative = array.findLast((element) => {
console.log("is negative?", element);
return element < 0;
});
console.log(lastNegative);
// 出力:
// is negative? 5
// is negative? 4
// is negative? 3
// is negative? 2
// is negative? 1
// undefined
find メソッド, findIndex メソッド, findLast メソッド, findLastIndex メソッドの特徴
配列の find メソッドと findLast メソッドは、特定の条件を満たす要素を 1 つだけ取り出してその要素の値を得る操作を行います。その条件はコールバック関数を使って自由に指定することができます。
find メソッドや findLast メソッドは、配列の要素の値だけでなく undefined を返す可能性もあります。
find メソッドや findLast メソッドが undefined を返した場合、2 つの可能性があります。
- もとの配列に条件を満たす要素が 1 つも存在しなかった場合
- 配列の要素の値として undefined が存在して、その要素が条件を満たした場合
- ここで「要素」には、プロパティが存在しない(空の)インデックスについても含まれます。
コールバック関数で指定した条件を undefined が必ず満たさない場合は、後者の可能性を除外できて、もとの配列に条件を満たす要素が存在しなかったことになります。
配列の findIndex メソッドと findLastIndex メソッドは、特定の条件を満たす要素を 1 つだけ取り出してその要素のインデックスを得る操作を行います。その条件はコールバック関数を使って自由に指定することができます。
findIndex メソッドや findLastIndex メソッドが −1 を返した場合は、もとの配列に条件を満たす要素が 1 つも存在しなかったことを意味します。
コールバック関数が truthy な値を返すと、その時点で反復が終了し、その要素の値またはインデックスが返されます。この動作は for 文での break 文または return 文に相当します。これにより、長い配列であっても、先頭付近に条件を満たす要素があれば配列の残りの要素についての反復がされないため、効率よく要素を取り出すことが可能です。
現在のイテレーションを途中で終了し、次のイテレーションへ進む(for 文での continue 文に相当する)には return 文で falsy な値を返します。
条件を満たした要素の値またはインデックスに興味がなく、条件を満たす要素が存在するかどうかだけを知りたい場合は、some メソッドや every メソッドを使うことができます。
反復対象となるインデックス
find メソッドや findIndex メソッドが反復しうる範囲は、メソッドが呼び出された時点での配列の長さをもとに決まります。そのため、find メソッドや findIndex メソッドのコールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されません。減らした場合は、もとの長さまで(undefined を値として)反復されます。
疎な配列のプロパティが存在しない(空の)インデックスについても反復されます。存在しないインデックスについてはコールバック関数の第 1 引数には undefined が渡されます。
const array = [1, , 3, 4, 5];
const firstEven = array.find((element) => {
console.log("is even?", element);
return element !== undefined && element % 2 === 0;
});
console.log(firstEven);
// 出力:
// is even? 1
// is even? undefined
// is even? 3
// is even? 4
// 4
find メソッド, findIndex メソッド, findLast メソッド, findLastIndex メソッドに渡すコールバック関数で非同期に真理値を返すことはできません。理由は filter メソッドと同様です。代わりに for 文と await 演算子を使った逐次の反復処理を行うことができます。
コールバック関数の第 3 引数は、メソッドチェーンでこれらのメソッドを呼び出す際に役立つことがあります。(forEach と同様)
find メソッド, findIndex メソッド, findLast メソッド, findLastIndex メソッドの第 2 引数で、コールバック関数の中で this
として使われる値を指定することができます。(forEach と同様)
配列の find メソッド, findIndex メソッド, findLast メソッド, findLastIndex メソッドは、配列風オブジェクトに対しても呼び出すことができます。
find メソッド, findIndex メソッド, findLast メソッド, findLastIndex メソッドの書き換え
array
が配列のとき、
let result = array.find((element) => {
const condition = <条件>;
return condition;
});
は <条件>
が array
の値を変更しないならば
let result = undefined;
for (let i = 0, length = +array.length; i < length; i++) {
let element = array[i];
const condition = <条件>;
if (condition) {
result = element;
break;
}
}
とだいたい同じ意味になります。(i
length
は <条件>
の中で参照されない変数名とする)
また、
let result = undefined;
for (let element of array) {
const condition = <条件>;
if (condition) {
result = element;
break;
}
}
ともだいたい同じ意味になります(find メソッドの実行中に array
の長さが増加したときに追加されたインデックスについても反復されるようになるという違いがあります)。
array
が配列のとき、
let result = array.findIndex((element) => {
const condition = <条件>;
return condition;
});
は <条件>
が array
の値を変更しないならば
let result = -1;
for (let i = 0, length = +array.length; i < length; i++) {
let element = array[i];
const condition = <条件>;
if (condition) {
result = i;
break;
}
}
とだいたい同じ意味になります。(i
length
は <条件>
の中で参照されない変数名とする)
また、
let result = -1;
for (let [i, element] of array.entries()) {
const condition = <条件>;
if (condition) {
result = i;
break;
}
}
ともだいたい同じ意味になります(findIndex メソッドの実行中に array
の長さが増加したときに追加されたインデックスについても反復されるようになるという違いがあります)。
array
が配列のとき、
let result = array.findLast((element) => {
const condition = <条件>;
return condition;
});
は同様に
let result = undefined;
for (let i = array.length - 1; i >= 0; i--) {
let element = array[i];
const condition = <条件>;
if (condition) {
result = element;
break;
}
}
とだいたい同じ意味になり、
let result = array.findLastIndex((element) => {
const condition = <条件>;
return condition;
});
は
let result = -1;
for (let i = array.length - 1; i >= 0; i--) {
let element = array[i];
const condition = <条件>;
if (condition) {
result = i;
break;
}
}
とだいたい同じ意味になります。(i
は <条件>
の中で参照されない変数名とする)
特定の値を検索するパターンの書き換え
密な配列 array
から特定の値を持つ要素のインデックスを取得する場合、
const search = <値>;
const index = array.findIndex((element) => element === search);
は
const search = <値>;
const index = array.indexOf(search);
// または
const index = array.indexOf(<値>);
とだいたい同じ意味になり、
const search = <値>;
const index = array.findLastIndex((element) => element === search);
は
const search = <値>;
const index = array.lastIndexOf(search);
// または
const index = array.lastIndexOf(<値>);
とだいたい同じ意味になります。
findIndex メソッドや findLastIndex メソッドを使う場合 <値>
が配列の各要素に対して繰り返し評価されるのを避けるために、上記のコード例のようにコールバック関数の外側で変数 search
に代入する場合がありますが、indexOf メソッドや lastIndexOf メソッドを使うと <値>
を直接引数に渡すこともできます。
flatMap メソッド
配列の flatMap メソッドは、配列の各要素に対して順にコールバック関数を実行し、その返り値が配列であればその配列の要素を、配列でない値であればその値自体を集めます。すべての要素に対して実行が終了すると、flatMap メソッドは集めた値からなる新しい配列を返します。もとの配列は変更されません。
const array = [1, 2, 3];
// 各要素の値をその数値の回数だけ繰り返す
const repeated = array.flatMap((element) => new Array(element).fill(element));
console.log(repeated); // 出力: [ 1, 2, 2, 3, 3, 3 ]
// もとの配列は変更されていない
console.log(array); // 出力: [ 1, 2, 3 ]
flatMap メソッドの特徴
配列の flatMap メソッドは、map メソッドと flat メソッドを組み合わせたような操作を行います。flat メソッドは、平たくいえば 2 次元配列を 1 次元配列に展開するような操作(平坦化)を行います。
const array = [1, 2, 3];
const array2 = array.map((element) => new Array(element).fill(element)); // [ [ 1 ], [ 2, 2 ], [ 3, 3, 3 ] ]
const flat = array2.flat(); // [ 1, 2, 2, 3, 3, 3 ] (flatMap メソッドの結果と同じ)
配列の map メソッドではもとの配列と結果の配列の長さは等しく、要素が 1 対 1 で対応します。それに対し、flatMap メソッドではもとの配列の 1 要素に対して結果の配列に複数の要素を生成したり、結果の配列に要素を含めない(0 個の要素を生成する)ことができます。
const books = [
{ title: "A", author: "X", tags: ["a", "b"] },
{ title: "B", author: "Y", tags: ["b", "c"] },
{ title: "C", author: "Z", tags: ["c", "d"] },
];
// タグ "b" を含む各書籍について、書名の行と著者の行を生成する
const result = books.flatMap((book) => {
if (!book.tags.includes("b")) {
return []; // 空の配列を返す(0 行を生成する)
}
// 長さ 2 の配列を返す(2 行を生成する)
return [
book.title,
`by ${book.author}`,
];
});
console.log(result); // 出力: [ 'A', 'by X', 'B', 'by Y' ]
配列でない値の取り扱い
コールバック関数の返り値が配列でない値(undefined を含む)だった場合、その値はそのまま結果の配列の 1 要素の値として含まれます。結果の配列に要素を含めない場合は、空の配列を返す必要があります。
const array = [1, 2, 3, 4, 5];
const result = array.flatMap((element) => {
if (element % 2 === 0) {
return element; // 2, 4 については数値をそのまま返す
}
if (element === 5) {
return; // 5 については undefined を返す
}
// それ以外はその数値の回数だけ繰り返す配列を返す
return new Array(element).fill(element);
});
console.log(result); // 出力: [ 1, 2, 3, 3, 3, 4, undefined ]
平坦化の深さ
コールバック関数の返り値の配列が 2 段階以上平坦化されることはありません。たとえば、コールバック関数が 2 次元配列を返した場合、flatMap メソッドの返り値は 2 次元配列になります。
const array = [
{ classroomName: "1", groups: [["A", "B"], ["C", "D"]] },
{ classroomName: "2", groups: [["E", "F"], ["G", "H"]] },
];
const result = array.flatMap((element) => element.groups);
console.log(result);
// 出力: [ [ 'A', 'B' ], [ 'C', 'D' ], [ 'E', 'F' ], [ 'G', 'H' ] ]
2 段階以上平坦化したい場合は、さらに flat メソッドを繋げるか、flatMap メソッドに渡すコールバック関数の返り値で flat メソッド(または flatMap メソッド)を使うことができます。
const array = [
{ classroomName: "1", groups: [["A", "B"], ["C", "D"]] },
{ classroomName: "2", groups: [["E", "F"], ["G", "H"]] },
];
const result = array.flatMap((element) => element.groups).flat();
console.log(result);
// 出力: [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' ]
const result2 = array.flatMap((element) => element.groups.flat());
console.log(result2);
// 出力: [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' ]
const result3 = array.flatMap((element) => element.groups.flatMap(
(group) => group.map((student) => `${element.classroomName}-${student}`
)));
console.log(result3);
// 出力: [ '1-A', '1-B', '1-C', '1-D', '2-E', '2-F', '2-G', '2-H' ]
コールバック関数の中で、ループ全体を途中で終了し、ループの直後に処理を移す(for 文での break 文や return 文に相当する)手段が無い点は forEach メソッドと同様です。
反復対象となるインデックス
flatMap メソッドが反復する範囲は、メソッドが呼び出された時点での配列の長さをもとに決まります。そのため、flatMap メソッドのコールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されず、新しい配列にも含まれません。
もとの配列のプロパティが存在しない(空の)インデックスについては反復されず、新しい配列ではインデックスが詰められます。
コールバック関数が返した配列のプロパティが存在しない(空の)インデックスについても、新しい配列に要素として含まれずインデックスが詰められます。
flatMap メソッドが返す配列は常に密な配列になります。
非同期処理を含むコールバック関数
flatMap メソッドに渡すコールバック関数で非同期に配列を返すことはできません。
/** 数値をその回数だけ繰り返した配列を返す非同期関数 */
const repeat = async (element) => {
// 1 秒かかる非同期処理を再現
await new Promise((resolve) => setTimeout(resolve, 1000));
return new Array(element).fill(element);
};
const array = [1, 2, 3];
const repeated = array.flatMap(async (element) => {
const result = await repeat(element);
return result;
});
console.log(repeated); // 出力: [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
これは、非同期関数の返り値が Promise オブジェクトになるためです。Promise オブジェクトは配列ではないため、そのまま(平坦化されずに)結果の配列に含まれます。
代わりに、for-of 文と await 演算子を使った逐次の反復処理を行うか、map メソッドと Promise.all
と flat メソッドを組み合わせることで並列処理を行うことができます。
// 略
const array = [1, 2, 3];
const repeated = [];
// 逐次処理を行う
for (const element of array) {
const result = await repeat(element);
if (Array.isArray(result)) {
repeated.push(...result);
} else {
repeated.push(result);
}
}
console.log(repeated); // 出力: [ 1, 2, 2, 3, 3, 3 ]
// 略
const array = [1, 2, 3];
const repeated = (
// map メソッドで非同期処理を並列実行し、Promise.all ですべての結果を待つ
await Promise.all(
array.map(async (element) => {
const result = await repeat(element);
return result;
})
)
)
.flat();
console.log(repeated); // 出力: [ 1, 2, 2, 3, 3, 3 ]
コールバック関数の第 3 引数は、メソッドチェーンで flatMap メソッドを呼び出す際に役立つことがあります。(forEach と同様)
flatMap メソッドの第 2 引数で、コールバック関数の中で this
として使われる値を指定することができます。(forEach と同様)
配列の flatMap メソッドは、配列風オブジェクトに対しても呼び出すことができます。返り値は配列になります。
コールバック関数の返り値が配列でない配列風オブジェクトだった場合、その配列風オブジェクトはそのまま(平坦化されずに)結果の配列の 1 要素の値として含まれます。
flatMap メソッドの書き換え
array
が配列のとき、
const newArray = array.flatMap((element) => {
const result = <式>;
return result;
});
は <式>
が array
の値を変更しないならば
const newArray = [];
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
let element = array[i];
const result = <式>;
newArray.push(...[result].flat());
}
とだいたい同じ意味になります。(i
length
は <式>
の中で参照されない変数名とする)
また、array
が密な配列のとき、
const newArray = [];
for (let element of array) {
const result = <式>;
newArray.push(...[result].flat());
}
ともだいたい同じ意味になります(flatMap メソッドの実行中に array
の長さが増加したときに、追加されたインデックスについても反復されるようになるという違いがあります)。
特に、<式>
の値が必ず密な配列になる場合は
newArray.push(...[result].flat());
の部分は
newArray.push(...result);
と書き換えることができます。
array
が密な配列で、<式>
の値が必ず配列でない値になる場合は flatMap メソッドの代わりに map メソッドを使ってもほとんどのケースで同じ結果が得られるはずです。
array
が配列のとき、コールバック関数が恒等関数であるならば
const newArray = array.flatMap((element) => element);
は
const newArray = array.flat();
と書き換えられます。
reduce メソッド, reduceRight メソッド
配列の reduce メソッドは、これまで紹介したメソッドとは異なり、コールバック関数に 4 つの引数を渡します。
- 累積値: これまでのイテレーションでの計算の結果
- 現在の要素の値: 現在のイテレーションで対象とする要素の値
- 現在のインデックス: 現在のイテレーションで対象とする要素のインデックス
- 配列: 反復対象の配列全体
また、reduce メソッドの第 2 引数には累積値の初期値を渡すことができます。
reduce メソッドは、配列の各要素に対して順にコールバック関数を実行します。コールバック関数の返り値は、次のイテレーションでコールバック関数に渡す累積値になります。最後のイテレーションが終了したときの返り値が reduce メソッドの返り値になります。
const array = ["Alice", "Bob", "Charlie"];
/** 配列の各要素の値である文字列の長さの合計を求める */
const totalLength = array.reduce(
// 累積値 acc は、これまで処理した文字列の長さの合計(累積和)
(acc, element, i) => {
console.log("acc:", acc, "element:", element, "index:", i);
// 現在の要素の値 element の長さを累積値に加算して返す
return acc + element.length;
},
0 // 累積値の初期値は 0
);
console.log(totalLength);
// 出力:
// acc: 0 element: Alice index: 0
// acc: 5 element: Bob index: 1
// acc: 8 element: Charlie index: 2
// 15
reduce メソッドの第 2 引数を省略すると、配列の先頭の要素の値が累積値の初期値として使われ、その先頭の要素のイテレーションはスキップされます。(第 2 引数として undefined を指定した場合は省略していない扱いになります)
const array = [1, 2, 3, 4, 5];
/** 数値からなる配列の要素の値の総和を求める */
const sum = array.reduce((acc, element, i) => {
console.log("acc:", acc, "element:", element, "index:", i);
return acc + element;
});
console.log(sum);
// 出力:
// acc: 1 element: 2 index: 1
// acc: 3 element: 3 index: 2
// acc: 6 element: 4 index: 3
// acc: 10 element: 5 index: 4
// 15
もとの配列が空配列のとき、コールバック関数は呼ばれることなく、reduce メソッドは第 2 引数をそのまま返します。このとき第 2 引数が省略されていた場合は実行時エラーになります。
もとの配列が 1 要素しか持たない配列で、reduce メソッドの第 2 引数が省略されている場合は、コールバック関数は呼ばれることなく、その 1 要素の値が reduce メソッドの返り値になります。
配列の reduceRight メソッドは、reduce メソッドと似ていますが、反復の順序が逆になる(末尾から先頭に向かって反復する)点が異なります。
コールバック関数の引数の順序は reduce メソッドと同じで、累積値が第 1 引数になります。
const operands = [2, 3, 3];
// 2 の (3 の 3 乗) 乗を計算する
const result = operands.reduceRight(
(acc, element, i) => {
console.log("acc:", acc, "element:", element, "index:", i);
return element ** acc;
},
1
);
console.log(result);
// 出力:
// acc: 1 element: 3 index: 2
// acc: 3 element: 3 index: 1
// acc: 27 element: 2 index: 0
// 134217728
reduceRight メソッドの第 2 引数を省略すると、配列の末尾の要素の値が累積値の初期値として使われ、その末尾の要素のイテレーションはスキップされます。
reduce メソッド, reduceRight メソッドの特徴
配列の reduce メソッドや reduceRight メソッドは、多数の要素を 1 つの値にまとめる操作を行います。関数型プログラミングにおいては畳み込み (fold) と呼ばれることがあります。
コールバック関数が第 1 引数
- reduce メソッドの返り値は
(\dotsm ((e \cdot a_0) \cdot a_1) \dotsm) \cdot a_{n - 1} - reduceRight メソッドの返り値は
(\dotsm ((e \cdot a_{n - 1}) \cdot a_{n - 2}) \dotsm) \cdot a_0
になります。reduceRight メソッドに渡すコールバック関数の第 1 引数と第 2 引数を逆にすると、上記の式はちょうど左右反転した形になり、配列の要素の値が左から右の順に並び、二項演算によって右から左に向かって畳み込まれることがわかります。
配列の要素の値が属する集合
たとえば文字列の集合において文字列の結合 (a, b) => a + b
と空文字列 ""
はモノイドをなします。
const array = ["a", "b", "c"];
const leftReduced = array.reduce((a, b) => a + b, "");
console.log(leftReduced); // 出力: abc
const rightReduced = array.reduceRight((b, a) => a + b, "");
console.log(rightReduced); // 出力: abc
あるイテレーションでコールバック関数が返した値は、次のイテレーションで同じコールバック関数に第 1 引数として渡されます。また、reduce メソッドや reduceRight メソッドに渡す第 2 引数も、最初のイテレーションでコールバック関数に第 1 引数として渡される値になります。
したがって、reduce メソッドや reduceRight メソッドの第 2 引数を指定する場合、通常は以下の 3 箇所が同種の値になる/同種の値を想定するようにします。
- コールバック関数が返す値(そのイテレーションまでの累積値)
- reduce メソッドや reduceRight メソッドに渡す第 2 引数(累積値の初期値)
- コールバック関数が受け取る第 1 引数(前のイテレーションまでの累積値)
この 3 箇所の中に「配列の要素の値」や「コールバック関数が受け取る第 2 引数」が含まれていないことに注意してください。つまり、配列の要素の値とは別種の値を累積値とすることができます。
たとえば、最初のコード例「配列の各要素の値である文字列の長さの合計を求める」では、配列の要素の値は文字列でしたが、上記 3 箇所の累積値は文字列の長さの合計を表す数値でした。
累積値の設計に自由度があるため、reduce メソッドや reduceRight メソッドはそれだけでさまざまな操作を行える万能な武器になります。
これまで紹介してきた反復のためのメソッドのほとんどは、reduce メソッドや reduceRight メソッドを使ってもだいたい同じような処理が行えます。(が、専用のメソッドを使ったほうが効率や可読性・記述量において優れていることが多いでしょう)
各種のメソッドを reduce メソッドで再現する
reduce メソッドや reduceRight メソッドの第 2 引数を省略する場合は、最初のイテレーションで第 1 引数に配列の先頭の要素の値が渡されるため、配列の要素の値がすべて同種であるならば通常は累積値も配列の要素の値と同種とすることになります。
コールバック関数の中で、ループ全体を途中で終了し、ループの直後に処理を移す(for 文での break 文や return 文に相当する)手段が無い点は forEach メソッドと同様です。
現在のイテレーションを途中で終了し、次のイテレーションへ進む(for 文での continue 文に相当する)には return 文を使います。第 1 引数で渡された値をそのまま返すことで、現在のイテレーションはスキップされたことになります。
反復対象となるインデックス
reduce メソッドや reduceRight メソッドが反復する範囲は、メソッドが呼び出された時点での配列の長さをもとに決まります。そのため、コールバック関数で配列の長さを増やした場合、もとの長さを超えるインデックスの要素については反復されません。
疎な配列のプロパティが存在しない(空の)インデックスについては反復されません。累積値がリセットされることもありません。
reduce メソッドや reduceRight メソッドの第 2 引数を省略して呼び出した場合、配列の長さが 0 であるか配列のインデックスがどれも存在しない場合は実行時エラー (TypeError) になります。
コールバック関数の第 4 引数は、メソッドチェーンでこれらのメソッドを呼び出す際に役立つことがあります。(forEach などでのコールバック関数の第 3 引数と同様)
reduce メソッド, reduceRight メソッドの書き換え
array
が配列のとき、
let result = array.reduce(
(acc, element) => {
const value = <式>;
return value;
},
<初期値>
);
は <式>
が array
の値を変更しないならば
let result = <初期値>;
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
let acc = result;
let element = array[i];
const value = <式>;
result = value;
}
とだいたい同じ意味になります。(i
length
は <式>
の中で参照されない変数名とする)
また、reduce メソッドの第 2 引数を省略した場合
let result = array.reduce(
(acc, element) => {
const value = <式>;
return value;
}
);
は <式>
が array
の値を変更しないならば
let initialized = false;
let result;
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
if (!initialized) {
result = array[i];
initialized = true;
continue;
}
let acc = result;
let element = array[i];
const value = <式>;
result = value;
}
if (!initialized) throw new TypeError();
とだいたい同じ意味になります。(i
length
は <式>
の中で参照されない変数名とし、initialized
はこのスコープで定義されていない変数名とする)
array
が配列のとき、
let result = array.reduceRight(
(acc, element) => {
const value = <式>;
return value;
},
<初期値>
);
は <式>
が array
の値を変更しないならば
let result = <初期値>;
for (let i = +array.length - 1; i >= 0; i--) {
if (!(i in array)) continue;
let acc = result;
let element = array[i];
const value = <式>;
result = value;
}
とだいたい同じ意味になります。(i
は <式>
の中で参照されない変数名とする)
また、reduceRight メソッドの第 2 引数を省略した場合
let result = array.reduceRight(
(acc, element) => {
const value = <式>;
return value;
}
);
は <式>
が array
の値を変更しないならば
let initialized = false;
let result;
for (let i = +array.length - 1; i >= 0; i--) {
if (!(i in array)) continue;
if (!initialized) {
result = array[i];
initialized = true;
continue;
}
let acc = result;
let element = array[i];
const value = <式>;
result = value;
}
if (!initialized) throw new TypeError();
とだいたい同じ意味になります。(i
は <式>
の中で参照されない変数名とし、initialized
はこのスコープで定義されていない変数名とする)
includes メソッド
配列の includes メソッドは、指定された値を配列の各要素の値と比較して、等しい要素があれば true を返します。どの要素の値とも等しくない場合は false を返します。
このメソッドにはコールバック関数を渡すことはできません。代わりに、第 1 引数には比較対象の値を指定します。
const array = ["a", "b", "c", "d", "e"];
array.includes("c") // true
array.includes("f") // false
第 2 引数には、比較を開始するインデックスを指定することができます。デフォルトは 0 です(すべての要素が比較されます)。この引数に負の整数値を指定すると、代わりに配列の末尾から数えます。
const array = ["a", "b", "c", "d", "e"];
array.includes("c", 2) // true (インデックス 2 の要素の値が "c" なので true)
array.includes("c", 3) // false (インデックス 3 以降に値が "c" の要素はないので false)
array.includes("c", -3) // true (末尾から数えて 3 番目 = インデックス 2 の要素から比較開始する)
array.includes("c", -2) // false (末尾から数えて 2 番目 = インデックス 3 の要素から比較開始する)
includes メソッドの特徴
配列の includes メソッドには、コールバック関数を渡せないため、反復処理の内容を指定することはできません。よく行われるタイプの反復処理のために特化したメソッドであると理解することができます。
includes メソッドは、指定された値が配列の中に存在するかどうかを調べるために使われます。
空配列に対して includes メソッドを呼び出すと、常に false が返ります。
配列の includes メソッドは等しいかどうかの判定で SameValueZero 抽象操作を使用します。
const sameValueZero = (a, b) => a === b || (a !== a && b !== b);
SameValueZero 抽象操作
SameValueZero 抽象操作では、===
演算子と異なり NaN と NaN は等しいとみなされます。(NaN === NaN
は false です)
const array = [1, NaN, 3, 4, 5];
array.includes(NaN) // 出力: true
一方で、0 と −0 が等しいとみなされます。(===
演算子と同様)
const array = [1, 0, 3, 4, 5];
array.includes(0) // true
array.includes(-0) // true
数値型以外については ===
演算子と同様の比較になります。型変換は行われません。
const array = [1, "2", 3, 4, 5];
array.includes("2") // true
array.includes(2) // false
比較するインデックスの範囲
第 2 引数に指定された値は、数値型 (Number) の整数に変換されます。その過程で NaN や −0 となる値は 0 として扱われ、配列の先頭から比較が開始します。
第 2 引数に指定された比較の開始位置が配列の長さ以上の場合、includes メソッドは常に false を返します。比較の開始位置が負の値で、その絶対値が配列の長さ以上の場合は、配列の先頭から比較を開始します。
比較の終了位置を指定する手段はありません。必要であれば slice メソッドなどで配列の一部分を取り出してから includes メソッドを呼び出すことができます。
疎な配列のプロパティが存在しない(空の)インデックスがある場合、undefined に対しても比較が行われます。
const array = [1, , 3, 4, 5];
array.includes(undefined) // true
配列の includes メソッドは、配列風オブジェクトに対しても呼び出すことができます。
includes メソッドにはコールバック関数を渡さないため、(処理にかかる時間などの観測を除いて)反復がされたことや反復の順序などをコードから観測できない場合があります。処理系はそのような場合に素朴な反復の代わりに高速なアルゴリズムを使うなどの最適化を行うかもしれません。
includes メソッドの書き換え
ここで ===
演算子を使っている部分は、NaN に対する比較で挙動が一致しません。厳密な書き換えにするには代わりに前述の sameValueZero
関数を使う必要があります。
array
が配列のとき、
const search = <値>;
let result = array.includes(search);
は
const search = <値>;
let result = array.findIndex((element) => element === search) !== -1;
や
const search = <値>;
let result = false;
for (let i = 0, length = +array.length; i < length; i++) {
if (array[i] === search) {
result = true;
break;
}
}
や
const search = <値>;
let result = false;
for (const element of array) {
if (element === search) {
result = true;
break;
}
}
とだいたい同じ意味になります。
array
が密な配列のとき、
const search = <値>;
const result = array.includes(search);
は
const search = <値>;
const result = array.some((element) => element === search);
とだいたい同じ意味になります。
array
が密な配列のとき、
const result = array.includes(<値>, <開始位置>);
は <値>
の値が NaN でない場合
const result = array.indexOf(<値>, <開始位置>) !== -1;
とだいたい同じ意味になります。(, <開始位置>
は省略可能です)
indexOf メソッド, lastIndexOf メソッド
配列の indexOf メソッドは、指定された値を配列の各要素の値と順に比較して、等しい要素があればそのインデックスを返します。どの要素の値とも等しくない場合は −1 を返します。
このメソッドにはコールバック関数を渡すことはできません。代わりに、第 1 引数には比較対象の値を指定します。
const array = ["a", "b", "c", "b", "a"];
array.indexOf("b") // 1
array.indexOf("d") // -1
等しい要素が複数ある場合、もっとも先頭に近いもののインデックスが返されます。
第 2 引数には、比較を開始するインデックスを指定することができます。デフォルトは 0 です(すべての要素が比較されます)。この引数に負の整数値を指定すると、代わりに配列の末尾から数えます。(includes メソッドと同様)
配列の lastIndexOf メソッドは、indexOf メソッドと似ていますが、配列の末尾から逆順に比較を行う点が異なります。したがって、等しい要素が複数ある場合、もっとも末尾に近いもののインデックスが返されます。
const array = ["a", "b", "c", "b", "a"];
array.lastIndexOf("b") // 3
array.lastIndexOf("d") // -1
第 2 引数には、比較を開始するインデックスを指定することができます。デフォルトは配列の長さ − 1 です(すべての要素が比較されます)。このインデックスから始めて先頭に向かって比較が行われます。この引数に負の整数値を指定すると、代わりに配列の末尾から数えます。
const array = ["a", "b", "c", "b", "a"];
array.lastIndexOf("b", 1) // 1 (インデックス 1 の要素の値が "b")
array.lastIndexOf("b", 0) // -1 (インデックス 0 までに値が "b" の要素はない)
array.lastIndexOf("b", -4) // 1 (末尾から数えて 4 番目 = インデックス 1 の要素から先頭に向かって比較開始する)
array.lastIndexOf("b", -5) // -1 (末尾から数えて 5 番目 = インデックス 0 の要素から先頭に向かって比較開始する)
indexOf メソッド, lastIndexOf メソッドの特徴
配列の indexOf メソッドや lastIndexOf メソッドには、コールバック関数を渡せないため、反復処理の内容を指定することはできません。よく行われるタイプの反復処理のために特化したメソッドであると理解することができます。
indexOf メソッドや lastIndexOf メソッドは、指定された値が配列の中に存在するかどうか・どの位置に存在するかを調べるために使われます。
空配列に対して indexOf メソッドや lastIndexOf メソッドを呼び出すと、常に −1 が返ります。
配列の indexOf メソッドや lastIndexOf メソッドは等しいかどうかの判定で ===
演算子と同じ IsStrictlyEqual 抽象操作を使用します。
IsStrictlyEqual 抽象操作
IsStrictlyEqual 抽象操作では、
- NaN はどの値とも等しくないとみなされます。(
NaN === NaN
は false です) - 0 と −0 は等しいとみなされます。(
0 === -0
は true です) - 型変換は行われません。
const array = [0, 1, NaN, 3, 4, 5];
array.indexOf(NaN) // -1 (NaN はどの値とも等しくないため、見つけられない)
array.lastIndexOf(NaN) // -1
array.indexOf(0) // 0
array.indexOf(-0) // 0
array.lastIndexOf(0) // 0
array.lastIndexOf(-0) // 0
array.indexOf(1) // 1
array.indexOf("1") // -1
array.lastIndexOf(1) // 1
array.lastIndexOf("1") // -1
比較するインデックスの範囲
第 2 引数に指定された値は、数値型 (Number) の整数に変換されます。その過程で NaN や −0 となる値は 0 として扱われ、配列の先頭から比較が開始します。
indexOf メソッドの場合は配列全体の要素が比較対象になりますが、lastIndexOf メソッドの場合はインデックスが 0 の要素だけが比較対象になります。
とくに、lastIndexOf の第 2 引数を省略した場合と undefined を指定した場合(0 として扱われる)の挙動が異なるため注意が必要です。
const array = ["a", "b", "c", "b", "a"];
array.lastIndexOf("b") // 3
array.lastIndexOf("b", undefined) // -1 (インデックスが 0 の要素の値 "a" だけが比較対象になる)
indexOf メソッドの第 2 引数に指定された比較の開始位置が配列の長さ以上の場合、indexOf メソッドは常に −1 を返します。比較の開始位置が負の値で、その絶対値が配列の長さ以上の場合は、配列の先頭から比較を開始します。
lastIndexOf メソッドの第 2 引数に指定された比較の開始位置が配列の長さ以上の場合、配列の末尾から先頭に向かって比較を開始します。比較の開始位置が負の値で、その絶対値が配列の長さより大きい場合は、lastIndexOf メソッドは常に −1 を返します。
比較の終了位置を指定する手段はありません。必要であれば slice メソッドなどで配列の一部分を取り出してから indexOf メソッドや lastIndexOf メソッドを呼び出すことができます。その際、slice メソッドによるインデックスのずれに注意してください。
疎な配列のプロパティが存在しない(空の)インデックスは比較において無視されます。
const array = [1, , 3, 4, 5];
array.indexOf(undefined) // -1
array.lastIndexOf(undefined) // -1
配列の indexOf メソッドや lastIndexOf メソッドは、配列風オブジェクトに対しても呼び出すことができます。
indexOf メソッドや lastIndexOf メソッドにはコールバック関数を渡さないため、(処理にかかる時間などの観測を除いて)反復がされたことや反復の順序などをコードから観測できない場合があります。処理系はそのような場合に素朴な反復の代わりに高速なアルゴリズムを使うなどの最適化を行うかもしれません。
indexOf メソッド, lastIndexOf メソッドの書き換え
array
が配列のとき、
const search = <値>;
let result = array.indexOf(search);
は
const search = <値>;
let result = -1;
for (let i = 0, length = +array.length; i < length; i++) {
if (!(i in array)) continue;
if (array[i] === search) {
result = i;
break;
}
}
とだいたい同じ意味になり、
const search = <値>;
let result = array.lastIndexOf(search);
は
const search = <値>;
let result = -1;
for (let i = +array.length - 1; i >= 0; i--) {
if (!(i in array)) continue;
if (array[i] === search) {
result = i;
break;
}
}
とだいたい同じ意味になります。
array
が密な配列のとき、
const search = <値>;
let result = array.indexOf(search);
は
const search = <値>;
let result = array.findIndex((element) => element === search);
や
const search = <値>;
let result = false;
for (const element of array) {
if (element === search) {
result = true;
break;
}
}
とだいたい同じ意味になり、
const search = <値>;
let result = array.lastIndexOf(search);
は
const search = <値>;
let result = array.findLastIndex((element) => element === search);
とだいたい同じ意味になります。
存在するかどうかの判定の書き換え
特定の値が存在するかどうかを判定するだけであれば、array
が密な配列のとき、
const search = <値>;
const result = array.indexOf(search) !== -1;
は
const search = <値>;
const result = array.some((element) => element === search);
とだいたい同じ意味になります。<値>
の値が NaN でない場合は、
const search = <値>;
const result = array.includes(search);
ともだいたい同じ意味になります。
Symbol.iterator
メソッド
entries メソッド, keys メソッド, values メソッド, 配列のこれらのメソッドは、それ単体で反復を行えるものではありませんが、反復処理において重要な役割を果たしているためここで紹介します。
これらのメソッドは共通して、引数を取らず、返り値として配列イテレーターと呼ばれるオブジェクトを返します。配列イテレーターはイテレーターの一種であり、これを利用すると配列の要素に関する値を順に得ることができます。
イテレーターから値を順に取り出すには、イテレーターの next メソッドを呼び出します。next メソッドは、値を生み出すときは { value: <値>, done: false }
の形のオブジェクトを返し、それ以上値を生み出さないときは { value: <返り値>, done: true }
の形のオブジェクトを返します。<返り値>
は反復の対象とはならず、配列イテレーターの場合は undefined になります。
イテレーターはそれ自身が反復可能なオブジェクトであるため、イテレーター it
が値を有限個生み出して終了する場合は式 [...it]
で生み出す値を 1 つの配列にまとめることができます。
配列の entries メソッドは、配列の各要素についてそのインデックスと値の対(2 つの要素を持つ配列)を順に生み出すイテレーターを生成して返します。
const array = ["a", "b", "c"];
const it = array.entries();
console.log([...it]); // 出力: [ [ 0, 'a' ], [ 1, 'b' ], [ 2, 'c' ] ]
配列の keys メソッドは、配列の各要素のインデックスを順に生み出すイテレーターを生成して返します。
const array = ["a", "b", "c"];
const it = array.keys();
console.log([...it]); // 出力: [ 0, 1, 2 ]
配列の values メソッドは、配列の各要素の値を順に生み出すイテレーターを生成して返します。
const array = ["a", "b", "c"];
const it = array.values();
console.log([...it]); // 出力: [ 'a', 'b', 'c' ]
配列の Symbol.iterator
メソッドは、values メソッドと同一です。
const array = ["a", "b", "c"];
const it = array[Symbol.iterator]();
console.log([...it]); // 出力: [ 'a', 'b', 'c' ]
console.log(array.values === array[Symbol.iterator]); // 出力: true
Symbol.iterator
メソッドの特徴
entries メソッド, keys メソッド, values メソッド, 配列の entries メソッドは要素のインデックスと値の両方を使う反復処理を行う際に役立ちます。keys メソッドはインデックスだけを使う反復処理を行う際に役立ちます。
特に、for-of 文で配列を反復対象とすると、配列の要素の値だけが取り出されます。entries メソッドを使うことで、インデックスと値の両方を取り出す反復処理ができます。または、keys メソッドを使うことで、インデックスだけを取り出す反復処理ができます。
const array = ["a", "b", "c"];
for (const [index, value] of array.entries()) {
console.log(index, value);
}
// 出力:
// 0 a
// 1 b
// 2 c
for (const index of array.keys()) {
console.log(index);
}
// 出力:
// 0
// 1
// 2
配列以外のオブジェクトにも同名のメソッドが存在することがあります。例えば、Map や Set にも entries メソッド, keys メソッド, values メソッドがあります。そのオブジェクトに含まれる要素に関する値を 1 つずつ取り出すための統一的な方法として、これらのメソッドが用意されています。
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
for (const [key, value] of map.entries()) {
console.log(key, value);
}
// 出力:
// a 1
// b 2
// c 3
const set = new Set(["a", "b", "c"]);
for (const [key, value] of set.entries()) {
console.log(key, value);
}
// 出力:
// a a
// b b
// c c
values メソッドは、この統一的な方法の一部として配列にも便宜上用意されているもので、values メソッドを呼び出すオブジェクトが配列であることがわかっている場合はわざわざ使う意味は薄いでしょう。
一方で配列の Symbol.iterator
メソッドは、配列が反復可能なオブジェクトであるために必要なメソッドです。for-of 文やスプレッド構文などで配列を反復処理する際には、暗黙裡にこのメソッドが呼び出されます。
for-of 文で配列の要素の値が順に取り出されるのは、配列の Symbol.iterator
メソッドが配列の要素の値を順に生み出すイテレーターを生成して返すためです。
配列イテレーターは next メソッドを呼ばれるたびに配列の長さを取得して終了判定を行います。そのため、反復中に配列の長さが増減しても、配列の末尾まで正しく値が生み出されます。
配列のプロパティが存在しない(空の)インデックスも反復対象になります。このとき、値は undefined になります。
const array = ["a", , "c"];
const it = array.entries();
console.log([...it]); // 出力: [ [ 0, 'a' ], [ 1, undefined ], [ 2, 'c' ] ]
配列の entries メソッド, keys メソッド, values メソッド, Symbol.iterator
メソッドは、配列風オブジェクトに対しても呼び出すことができます。
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
const it = Array.prototype.entries.call(arrayLike);
console.log([...it]); // 出力: [ [ 0, 'a' ], [ 1, 'b' ], [ 2, 'c' ] ]
Array.from メソッド
Array コンストラクタの from メソッドは、反復可能なオブジェクトや配列風オブジェクトを新しい配列に変換して返します。
このメソッドは配列のメソッドではないことに注意してください。Array.from(o)
の形で呼び出されます。
このメソッドは 1 つの必須引数と 2 つの省略可能引数を取ります。必須である第 1 引数には変換対象となる反復可能なオブジェクトや配列風オブジェクトを指定します。
// Set は反復可能なオブジェクトです
const iterable = new Set(["a", "b", "c"]);
console.log(iterable); // 出力: Set(3) { 'a', 'b', 'c' }
const array = Array.from(iterable);
console.log(array); // 出力: [ 'a', 'b', 'c' ]
// 配列風オブジェクト
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
const array = Array.from(arrayLike);
console.log(array); // 出力: [ 'a', 'b', 'c' ]
from メソッドの第 2 引数には、各要素に対して順に呼び出されるコールバック関数を指定できます。このコールバック関数は配列の map メソッドに渡す関数と似て、返り値は新しい配列に追加される要素の値になります。ただし、コールバック関数には 2 つの引数が渡されます。コールバックの第 1 引数には変換対象の要素が、第 2 引数にはその要素のインデックスが渡されます。
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
const array = Array.from(arrayLike, (element, index) => {
console.log(element, index);
return element.toUpperCase(); // 新しい配列の要素の値には大文字に変換した文字列を使う
});
console.log(array);
// 出力:
// a 0
// b 1
// c 2
// [ 'A', 'B', 'C' ]
返り値は密な配列になります。その長さは、from メソッドの第 1 引数が反復可能なオブジェクトである場合、反復可能なオブジェクトから取り出された要素の個数になります。第 1 引数が反復可能でない配列風オブジェクトである場合、その長さに等しくなります。
Array.from メソッドの特徴
Array.from メソッドは、反復可能なオブジェクトや配列風オブジェクトを配列に変換するためのメソッドです。
反復可能なオブジェクトは Symbol.iterator
メソッドを持つオブジェクトです。そのままでも for-of 文での反復処理は可能ですが、配列に存在するメソッドを使って処理を行いたい場合や、インデックスを使って要素にアクセスしたい場合など、配列への変換が必要な場合に Array.from メソッドが使われます。配列風オブジェクトに関しても、配列に存在するメソッドを使って処理を行いたい場合などに Array.from メソッドが使われます。
Array.from に配列風オブジェクトを渡した場合、そのオブジェクトのプロパティが存在しない(空の)インデックスについても反復対象になります。
const arrayLike = { 0: "a", 2: "c", length: 3 };
const array = Array.from(arrayLike);
console.log(array); // 出力: [ 'a', undefined, 'c' ]
const array2 = Array.from(arrayLike, (element, index) => {
console.log(element, index);
return element?.toUpperCase();
});
console.log(array2);
// 出力:
// a 0
// undefined 1
// c 2
// [ 'A', undefined, 'C' ]
この性質を利用して、特定の長さの密な配列を Array.from メソッドで生成することがあります。
const length = 5;
const array = Array.from({ length }, (_, index) => index);
console.log(array); // 出力: [ 0, 1, 2, 3, 4 ]
// new Array(length) では要素のプロパティが存在しない疎な配列が生成されるため、map メソッドを使っても要素が生成されない
const array2 = new Array(length).map((_, index) => index);
console.log(array2); // 出力: [ <5 empty items> ]
const rows = 3;
const columns = 4;
// 2 次元配列を生成する
const array = Array.from({ length: rows }, () => {
return new Array(columns).fill(0);
});
console.log(array);
// 出力: [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0 ] ]
配列もまた反復可能なオブジェクトであるため、Array.from メソッドの第 1 引数に渡すことができます。この場合、返り値は新しい配列になります。
const array = ["a", "b", "c"];
const newArray = Array.from(array);
console.log(newArray); // 出力: [ 'a', 'b', 'c' ]
console.log(array === newArray); // 出力: false
// 疎な配列は密な配列に変換される
const sparseArray = ["a", , "c"];
const newArray = Array.from(sparseArray, (element, index) => {
return element?.toUpperCase();
});
console.log(newArray); // 出力: [ 'A', undefined, 'C' ]
Array.from メソッドに渡すコールバック関数を非同期関数にすると、from メソッドの返り値は Promise からなる配列になります。これについては配列の map メソッドと同様です。
Array.from メソッドの第 1 引数に反復可能でない配列風オブジェクトを渡した場合、Array.from メソッドが反復する範囲と返り値となる新しい配列の長さは、メソッドが呼び出された時点での配列風オブジェクトの長さをもとに決まります。そのため、コールバック関数で配列風オブジェクトの長さを増やした場合、もとの長さを超えるインデックスの要素については反復されず、新しい配列にも含まれません。
Array.from メソッドの第 3 引数で、コールバック関数の中で this
として使われる値を指定することができます。(配列の forEach メソッドの第 2 引数と同様)
Array.from メソッドは、Array 以外のコンストラクタに対しても呼び出すことができます。その場合、返り値はそのコンストラクタのインスタンスになります。
const iterable = new Set(["a", "b", "c"]);
// 配列風オブジェクトを生成
const array = Array.from.call(Object, iterable);
console.log(array); // 出力: { '0': 'a', '1': 'b', '2': 'c', length: 3 }
class ArraySubclass extends Array {}
const arraySubclass = ArraySubclass.from(iterable);
console.log(arraySubclass); // 出力: ArraySubclass(3) [ 'a', 'b', 'c' ]
Array.from メソッドの書き換え
iterable
が反復可能なオブジェクトのとき、
const array = Array.from(iterable);
は
const array = [...iterable];
や
const [...array] = iterable;
や
const array = [];
for (const element of iterable) {
array.push(element);
}
とだいたい同じ意味になります。
iterable
が反復可能なオブジェクトのとき、
const array = Array.from(iterable, (element) => {
const result = <式>;
return result;
});
は
const array = [];
for (const element of iterable) {
const result = <式>;
array.push(result);
}
とだいたい同じ意味になります。
arrayLike
が配列風オブジェクトのとき、
const array = Array.from(arrayLike);
は
const array = [];
for (let i = 0, length = +arrayLike.length; i < length; i++) {
array.push(arrayLike[i]);
}
や
const array = Array.prototype.slice.call(arrayLike, 0).fill(undefined);
や
const array = [...Array.prototype.values.call(arrayLike)];
とだいたい同じ意味になります。
Discussion