【JavaScript】ループ処理をマスターしよう
はじめに
プログラミング始めたての頃、JavaScript の書籍とか読んでるとこんなこと思いませんか?
「JavaScript ってループのやり方多すぎてどこで何を使えばいいかよくわからん」
かく言う僕もついこの前まではとりあえず生!のノリで forEach を乱用しまくり野郎だった頃が記憶に新しいです。
今回はそんな方に向けて状況によって適切なループ処理を選択できるようになるための、いちアプローチになればと思いこの記事を書きました。
参考になれば幸いです 🙏
ループ処理の選択基準
まず、以下 3 つの分類で扱うループを振り分けるのが良いと思います。
- シンプルに配列やオブジェクトをループしたいとき
- ループの要素ごとに条件を設け配列を加工したいとき
- ループの要素ごとに真偽値を取得したいとき
3 つの大カテゴリーで分けることができましたら以下のカテゴリーからループを選択します。
-
シンプルに配列やオブジェクトをループしたいとき
- while / do...while
- for
- for...of
- for...in
- Array.forEach
-
ループの要素ごとに条件を設け配列を加工したいとき
- Array.map
- Array.filter
- Array.reduce
- Array.find
-
ループの要素に対して真偽値を取得したいとき
- Array.every
- Array.some
こちらをベースにそれぞれの動きや特徴を確認していきます。
while / do...while
whileとdo...whileは条件式が true である間ループを繰り返し、条件式が false、または break したときに処理を終了します。
以下のサンプルをご確認ください。
let i = 0;
while (i < 5) {
console.log(`iの値は ${i}`);
i++;
}
let i = 0;
do {
console.log(`iの値は ${i}`);
i++;
} while (i < 5);
/* while do...while共通
iの値は 0
iの値は 1
iの値は 2
iの値は 3
iの値は 4
*/
ループ内で i をインクリメントして条件式 i < 5 が false になるまでループを繰り返します。
一見、同じ動きに見えますがwhile は前置判断で do...while は後置判断であるという違いは覚えておきましょう。
以下のサンプルを確認ください。
let i = 5;
while (i < 5) {
console.log(`iの値は ${i}`);
i++;
}
// 出力なし
let i = 5;
do {
console.log(`iの値は ${i}`);
i++;
} while (i < 5);
// iの値は 5
前置判断である while は i の値が 5 なのですでに条件式は false のため実行されない。後置判断である do...while は do の処理を実行してから条件式を見るので console が出力されています。
また、次の項目で紹介します for と同様にbreakとcontinueを使用することができます。
for
条件式が true の間ループする while と do...while に対して、forは指定された回数分だけくり返し処理を行う命令文です。
for (let i = 0; i < 5; i++) {
console.log(`iの値は ${i}`);
}
/*
iの値は 0
iの値は 1
iの値は 2
iの値は 3
iの値は 4
*/
馴染みのある感じですね。
for は「初期化式」「ループ継続条件式」「増減式」の 3 つの式でループを制御するので回数指定の場合は while などと比べるとスッキリした感じにはなります。
ただ今時の JavaScript で見るとどこか野暮ったい感じで、最近ではあまり目にすることがなくなってきたように感じます。(処理速度的には圧倒的に速いんですけどね)
また、for 文ではbreakとcontinueが使うことができます。
それぞれサンプルで確認します。
break
break は特定の条件を満たした場合に、ループを強制的に中断したいときに使用します。
掛け算の九九をサンプルに値が 30 を超えたら九九をやめるというサンプルで確認していきます。
let isBreak = false;
for (let i = 1; i <= 9; i++) {
for (let j = 1; j <= 9; j++) {
const result = i * j;
console.log(`${i} × ${j} = ${result}`);
if (result >= 30) {
isBreak = true;
break;
}
}
if (isBreak) break;
}
/*
1 × 1 = 1
1 × 2 = 2
1 × 3 = 3
中略
4 × 8 = 32
*/
最初の for 文が n 段の掛け算の数字でネストされた for で 1 から 9 の積を求め、結果が 30 を超えたらループを中断しています。
ただこのサンプル、ループ中断の条件がネストされた for の中にあります。
ネストされた for で単純に break しただけでは親のループは中断しません。親のループも一緒に中断するためにサンプルでは**if(isBreak) break;**を記述しています。
ちょっと読みにくいですね。
そんな時はラベル構文が便利です。以下のサンプルを確認ください。
kuku: for (let i = 1; i <= 9; i++) {
for (let j = 1; j <= 9; j++) {
const result = i * j;
console.log(`${i} × ${j} = ${result}`);
if (result >= 30) break kuku;
}
}
ループの先頭に**ラベル名:**とラベルを指定することで for 全体に任意の名前をつけることができます。
このラベル名を break するとネストされた for のどこで break してもループをその段階で中断することができるようになります。
continue
continue は現在のループだけをスキップして、次のループを継続して続行したいときに使用します。
プログラミングの学習するときの定番である FizzBuzz の簡単バージョンとして「1 から 15 の数字で 3 の倍数で Fizz と出力し、それ以外は数字を出力する」というサンプルで確認していきます。
スッと思いつく限りだと以下のような書き方になるかと思います。
for (i = 1; i <= 15; i++) {
if (i % 3 === 0) {
console.log("Fizz");
} else {
console.log(i);
}
}
これでも全然オッケーです。
ただ、if の処理がもっと長いときの最後に else があったり、その中でさらに if があったりと、つまるとこネストが深い if の場合可読性が著しく落ちてしまいます。
そんなときに continue を使って以下のような書き方に変えてあげたりすると if がスッキリおさまってコードとしても読みやすくなったりします。
for (i = 1; i <= 15; i++) {
if (i % 3 === 0) {
console.log("Fizz");
continue;
}
console.log(i);
}
動作としては同じになった上で else をなくせました。
for...of
配列ループの方法でES6
で追加されたのがfor...ofです
for...of は配列だけではなく Array ライクなNodeList、arguments、その他イテレータ / ジェネレーターなどの列挙可能なオブジェクトに対しても使用することができます。
今回は基本的な使い方を説明します。以下のサンプルを確認ください。
const array = ["apple", "orange", "banana"];
for (const value of array) {
console.log(value);
}
/*
apple
orange
banana
*/
こんな感じで for...of は仮変数(サンプルでいうconst value)に配列の各要素が格納されます。
for の増減式のように現在何番面の配列の要素に対して処理を行なっているかを確認する方法は後ほどご説明します。
また、for と同様にbreakとcontinueも使用可能です。
for...in
for...inはここまで紹介したループ処理とは少々毛色が異なります。
for...in の使い所は主にオブジェクトのプロパティをループしたいときに使用します。
逆に for...of などではオブジェクトのループができません。(正確にはできなくはないのですが基本的な構文ではできない)
サンプルでその挙動を確認ください。
const obj = { apple: 150, orange: 100, banana: 120 };
for (const key in obj) {
console.log(`${key} = ${obj[key]}`);
}
/*
apple = 150
orange = 100
banana = 120
*/
for (const value of obj) {
console.log(value);
}
// TypeError: obj is not iterable
for...in ではオブジェクトをループできて for...of ではエラーになることが確認できます。
for...in は仮変数keyにオブジェクトのプロパティ名が格納され、その値を使い**オブジェクト[プロパティ名]**のようにして使用します。
また、for と同様にbreakとcontinueも使用可能です。
配列で for...in は使用しない
for...in はオブジェクトのループのときに使用すると言いましたが、配列では使用できないのでしょうか?
答えとしては可能ですが、その仕様からあまり使用するべきではありません。公式にもその記載がございます。
サンプルで確認していきます。
const array = ["apple", "orange", "banana"];
Array.prototype.hoge = function () {};
for (const key in array) {
console.log(array[key]);
}
/*
apple
orange
banana
[Function (anonymous)]
*/
ご覧のように配列自体の prototype に機能を拡張してしまうとその拡張した機能までも出力してしまいます。
また、for...in は順番の保証がありません。
そもそも for...in うんぬんの前にオブジェクト(他言語でいうハッシュや連想配列)などは元々順番が担保されるものではありません。
順番を必ず守りたいのであれば整数値の名前でループする配列を使用すべきです。
以下にその一例のサンプルを載せます。
const obj = {
"00": "a",
"01": "b",
"02": "c",
"03": "d",
"04": "e",
"05": "f",
"06": "g",
"07": "h",
"08": "i",
"09": "j",
10: "k",
11: "l",
12: "m",
};
for (const key in obj) {
console.log(`${key} = ${obj[key]}`);
}
/*
10 = k
11 = l
12 = m
00 = a
01 = b
02 = c
03 = d
04 = e
05 = f
06 = g
07 = h
08 = i
09 = j
*/
0 パッドのプロパティ名にアルファベットを順番に羅列してみましたが、出力の順番が正しくないですね。
また、配列での for...in は仮変数にインデックス番号が格納されますが、番号が文字列数値で格納されることも覚えておきましょう。
以下、サンプルをご確認ください。
const array = ["apple", "orange", "banana"];
for (const key in array) {
console.log(key + 1);
}
/*
01
11
21
*/
期待値としてはインデックス番号にプラス 1 しているので 1,2,3 と返ってきてほしいのですが、文字列数値なので文字列連結されて 01,11,21 となってしまう例です。
for...in を for...of に変更
先ほど for...of ではオブジェクトをループできないので for...in を使用すると言いましたが、for...of でもオブジェクトをループすることも可能です。
よくあるやり方としては 2 パターンあります。
一つ目はObject.keysと組み合わせるやり方です。
以下のサンプルをご確認ください。
const obj = { apple: 150, orange: 100, banana: 120 };
console.log(Object.keys(obj));
for (const key of Object.keys(obj)) {
console.log(`${key} = ${obj[key]}`);
}
/*
[ 'apple', 'orange', 'banana' ]
apple = 150
orange = 100
banana = 120
*/
Object.keys は引数にオブジェクトを渡すとプロパティ名の配列を返します。
配列なら for...of でもループできるので、各要素ごとに元のオブジェクトのプロパティにアクセスするようにすれば for...in と同じ動きになります。
もうひとつ、Object.entriesと組み合わせることでも実現できます。
以下のサンプルを確認ください。
const obj = { apple: 150, orange: 100, banana: 120 };
console.log(Object.entries(obj));
for (const [key, value] of Object.entries(obj)) {
console.log(`${key} = ${value}`);
}
/*
[ [ 'apple', 150 ], [ 'orange', 100 ], [ 'banana', 120 ] ]
apple = 150
orange = 100
banana = 120
*/
Object.entries は引数に渡したオブジェクトを**[ プロパティ名, 値 ]**の形式の配列で返します。
その返り値を for...of の仮変数(const [key, value])で分割代入して受け取ることができます。こっちの方が個人的にはスッキリしてて好みです。
また、Array.entriesを使うことで for...of の仮変数にループのインデックスを格納することもできます。
以下、サンプルを確認ください。
const array = ["apple", "orange", "banana"];
console.log(array.entries().next().value);
for (const [index, element] of array.entries()) {
console.log(`${index} = ${element}`);
}
/*
[ 0, 'apple' ]
0 = apple
1 = orange
2 = banana
*/
**[数値型インデックス, 要素]**で配列が返りますので、オブジェクトの時と同様に for...of の仮変数を用意してあげれば OK です。
通常の for...of は仮変数に要素の値しか入らず、通常の for のように増減式でのインデックスが取れない点が場合によっては不便なのですがこれで解消できます。
そんなこんなで for 系のループは for...of でまかなえるので基本は for...of を使うというスタンスで良いのではと思います。
for...of は使うべきではない?
先ほど、for...of を基本使うのが良いと言った矢先になんですが、for...of は使わない方が良いというシチュエーションがあります。
それは ESLint を導入している場合です。
ESLint とは JavaScript のコードフォーマッターで構文やインデントが正しくないとエラーでお知らせしてくれるプロジェクトコードの品質を担保するための仕組みです。
ESLint によると for...of は Babel で変換すると regenerator-runtime が必要になるため、変換後のコードが巨大になります。利点欠点を比較すると、そこまでして使う利点はないよねという見解があるようです。
また、for...of に限らず for 系のループ自体を避けてこれから紹介する map や filter などそれぞれの用途にあったメソッドを選択しようねという方針のようです。
そんなこんなで、知らず知らず for...of を使い、コミット直前に ESLint で検証したときに大量のエラー出てげんなりする。
そんなこともあり得ますので頭の片隅にでも置いといていただくと良いのかなーなんて思います。
Array.forEach
Array.forEachは配列の各要素に対し、順番にメソッド内に記述した callback を実行します。
callback に与えられる引数は以下です。
- 第 1 引数:配列 n 番目の要素の値
- 第 2 引数:第 1 引数の index(省略可)
- 第 3 引数:元の配列(省略可)
返り値はundefinedです。
動作を確認していきます。以下、サンプルを確認ください。
const array = ["a", "b", "c"];
array.forEach((element, index, self) => {
// 最初だけ元の配列を出力
if (index < 1) console.table(self);
console.log(`index ${index} 番目の値 => ${element}`);
});
/*
┌─────────┬────────┐
│ (index) │ Values │
├─────────┼────────┤
│ 0 │ 'a' │
│ 1 │ 'b' │
│ 2 │ 'c' │
└─────────┴────────┘
index 0 番目の値 => a
index 1 番目の値 => b
index 2 番目の値 => c
*/
各引数の値が取得できていることを確認できます。
要素を追加・削除したとき
次に、Array.forEach が実行された後に要素を追加したり削除したりするとどうなるかをサンプルで確認してみます。
// forEachの途中で要素を追加してもcallbackには適用されない
const array = ["a", "b", "c"];
array.forEach((element, index, self) => {
if (index < 1) array.push("d");
console.log(`index ${index} 番目の値 => ${element}`);
});
console.log(array);
/*
index 0 番目の値 => a
index 1 番目の値 => b
index 2 番目の値 => c
[ 'a', 'b', 'c', 'd' ]
*/
// 削除された要素は参照しない
const array = ["a", "b", "c"];
array.forEach((element, index, self) => {
if (index < 1) array.splice(2, 1);
console.log(`index ${index} 番目の値 => ${element}`);
});
console.log(array);
/*
index 0 番目の値 => a
index 1 番目の値 => b
[ 'a', 'b' ]
*/
// すでに参照された要素を削除した場合次の要素は飛ばされる
const array = ["a", "b", "c"];
array.forEach((element, index, self) => {
console.log(`index ${index} 番目の値 => ${element}`);
if (index < 1) array.splice(0, 1);
});
console.log(array);
/*
index 0 番目の値 => a
index 1 番目の値 => c
[ 'b', 'c' ]
*/
1 つ目のサンプルは要素を追加した場合は Array.forEach の実行時には参照されないが、元の配列には要素が追加されていることが確認できます。
2 つ目のサンプルは Array.forEach の実行した最初に配列の最後の要素を削除しています。削除した要素は参照しないことが確認できます。
3つ目のサンプルは 1 つ目の要素を処理後に処理済みの 1 つ目の要素を削除しています。すると次の 2 番目の要素が参照されず 3 つ目の要素が参照されていることが確認できます。
これは配列の要素を削除したことで要素のインデックスが左づめになって 1 ずつずれたことによる動きです。
this の使用
Array.forEach の callback に this の値を渡すサンプルです。以下のサンプルをご確認ください。
function Counter() {
this.sum = 0;
this.count = 0;
}
Counter.prototype.add = function (array) {
array.forEach(function (entry) {
this.sum += entry;
++this.count;
}, this);
};
const obj = new Counter();
obj.add([2, 5, 9]);
console.log(obj.count);
console.log(obj.sum);
/*
3
16
*/
Counterというオブジェクトを生成し、prototype にaddという forEach の関数の機能を拡張した後にメソッドを呼び出してます。
実行結果で this の値を参照してそれぞれ値が変更されることが確認できます。
もちろん callback で this を渡さなければ参照できません。
function Counter() {
this.sum = 0;
this.count = 0;
}
Counter.prototype.add = function (array) {
array.forEach(function (entry) {
this.sum += entry;
++this.count;
});
};
const obj = new Counter();
obj.add([2, 5, 9]);
console.log(obj.count);
console.log(obj.sum);
/*
0
0
*/
ですが、callback の記述をES6
で追加されたアロー関数に変更すると this を省略しても参照できます。
function Counter() {
this.sum = 0;
this.count = 0;
}
Counter.prototype.add = function (array) {
array.forEach((entry) => {
this.sum += entry;
++this.count;
});
};
const obj = new Counter();
obj.add([2, 5, 9]);
console.log(obj.count);
console.log(obj.sum);
/*
3
16
*/
もちろん省略しないで this を明示的に渡しても動作します。
省略記法
Array 系のメソッドの良さの 1 つはアロー関数と組み合わせることで、省略して 1 行でコードを書けるスマートさです。
以下のサンプルをご確認ください。
// 基本系
array.forEach((element, index) => {
console.log(`index ${index} の値 ${element}`);
});
// 省略記法1:{}を省略
array.forEach((element, index) =>
console.log(`index ${index} の値 ${element}`)
);
// 省略記法2:引数が1つなら()も省略できる
array.forEach((element) => console.log(element));
コメント通りですが、基本波かっこ{}は省略可能です。また、引数が 1 つだけの場合は()も省略できるのでよりスッキリしますね。
ただ、3 つ目の書き方は人によって書く人、書かない人で表記がブレる原因になりますのでコード規約などで定義しておく方が良いと思います。
prettier で、コーディング中は各々好きな書き方しても最終的には整えてくれる。みたいな仕組みを導入しておくなども良いと思います。
break と continue は使えない
配列操作として Array.forEach はfor より手軽にスマートにかける反面、弱点としては break と continue ができないという欠点があります。
以下のサンプルのように return を使うことで continue のような動作を表現することは可能です。
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
array.forEach((element, index) => {
if ((index + 1) % 3 !== 0) return;
console.log(`${index + 1}は3の倍数です`);
});
/*
3は3の倍数です
6は3の倍数です
9は3の倍数です
*/
ただ break は仕様上どう頑張ってもできません。ネットなどで検索すればArray.someとの組み合わせで break と同じ動きをさせるなどの記事もありますが、推奨できるものではないのでここでは書きません。
言ってしまえば return で continue する書き方もあまり推奨できません。この場合だと後述しますArray.filterで実装するのが適切です。
冒頭で break と continue が使えないことが弱点と言いましたが、代わりになるメソッドはありますので Array.forEach はあくまですでに加工が終了した配列をシンプルにループする時に使うメソッドと割り切ってその他は別の方法でやるという風に実装していくのが良いかと思います。
Array.map
Array.mapは配列の各要素に対し、順番にメソッド内に記述した callback の結果を返します。
callback に与えられる引数は以下です。
- 第 1 引数:配列 n 番目の要素の値
- 第 2 引数:第 1 引数の index(省略可)
- 第 3 引数:元の配列(省略可)
返り値はcallback の結果から作成される新しい配列です。
動作を確認していきます。以下、サンプルを確認ください。
const array = [1, 2, 3];
const newArray = array.map((element, index, self) => {
return element * 2;
});
console.log(array);
// [ 1, 2, 3 ]
console.log(newArray);
// [ 2, 4, 6 ]
元の配列から各要素を 2 倍にした新しい配列が生成されていることを確認できます。
また、配列やオブジェクトは参照型ですのでそのまま代入すると参照渡しになりますが、map は新しい配列として生成されるので参照渡しの影響を受けません。
以下のサンプルを確認ください。
const array = [1, 2, 3];
const copy = array;
const newArray = array.map((element, index, self) => {
//if(index < 1) array.splice(2, 1);
return element * 2;
});
array[0] = 999;
console.log(array);
// [ 999, 2, 3 ]
// copyはarrayをそのまま代入したので参照渡し
console.log(copy);
// [ 999, 2, 3 ]
// newArrayはmapで生成されたので影響を受けない
console.log(newArray);
// [ 2, 4, 6 ]
このように map の使い所は callback から返された全く新しい配列を生成したいという時に使うのが適しています。
逆に返された配列を使用しない、callback から値を返さない場面などでは適していません。
省略記法も Array.forEach と同様の書き方ができます。違う点があるとしたら map の場合は値を返す必要があるので return の扱いですが、こちらは必ず省略してください。
サンプルで動作を確認します。
const array = [1, 2, 3];
const newArray = array.map(element => element * 2);
console.log(newArray);
// [ 2, 4, 6 ]
// returnを書くとSyntaxErrorになる
const newArray = array.map(element => return element * 2);
// SyntaxError: Unexpected token 'return'
return を書くとエラーになることが確認できます。
また、callback に this の変数を渡すこともできます。この時の挙動は Array.forEach と同様です。
要素を追加・削除したとき
Array.forEach 同様に map が呼び出された後に追加された要素に対して callback は実行されず、削除された要素に対しても callback は実行されません。
サンプルで動作を確認します。
// サンプル1
const array = [1, 2, 3];
const newArray = array.map((element, index) => {
if (index < 1) array.push(4);
return element;
});
console.log(newArray);
// [ 1, 2, 3 ]
console.log(array);
// [ 1, 2, 3, 4 ]
// サンプル2
const array = [1, 2, 3];
const newArray = array.map((element, index) => {
if (index < 1) array.splice(2, 1);
return element;
});
console.log(newArray);
// [ 1, 2, empty ]
console.log(array);
// [ 1, 2 ]
要素を追加した場合、新しく作成された配列には値がプッシュされず参照元の配列のみ変更されていることが確認できます。
配列の要素を削除した場合、元の配列からは最後の要素が削除され、新しい配列にはemptyという値が入ってます。
この empty は null や undefined とはまた別の空を表すリテラルになります。この配列をループすると empty の要素は undefined を表示します。
メソッドチェーン
個人的に Array 系のメソッドで最も強力な機能はこのメソッドチェーンだと思います。
まずはサンプルで動作を確認してみましょう。
const jumpComics = [
{ title: "呪術廻戦", author: "芥見下々" },
{ title: "ONE PIECE", author: "尾田栄一郎" },
{ title: "鬼滅の刃", author: "吾峠呼世晴" },
];
jumpComics.map((v) => v.title).forEach((element) => console.log(element));
/*
呪術廻戦
ONE PIECE
鬼滅の刃
*/
配列の中にジャンプの作品名と作者名のオブジェクトが格納されており Array.map で作品名のみ抽出します。
Array.map は新しい配列を返しますのでそのまま配列操作系のメソッドをドット(.)で繋ぐメソッドチェーンにより連続して使用することができます。
このメソッドチェーンは後述します他のArray.filterやArray.reduceなどでも使用することができます。
ただ、Array.forEach に関しては戻り値が undefined のためメソッドチェーンの最後に持ってくることはできても**Array.forEach().map()**のような書き方はできませんので注意してください。
また、サンプルのような配列オブジェクトをループするときは obj.property
のようにしてアクセスしますが、分割代入を組み合わせるとより処理の意図を伝えやすくなります。
const jumpComics = [
{ title: "呪術廻戦", author: "芥見下々" },
{ title: "ONE PIECE", author: "尾田栄一郎" },
{ title: "鬼滅の刃", author: "吾峠呼世晴" },
];
jumpComics.map(({ title }) => title).forEach((element) => console.log(element));
/*
呪術廻戦
ONE PIECE
鬼滅の刃
*/
少し余談ですが、React なども一緒に勉強したいと思ってる方、この分割代入はめっちゃ使うので合わせてチェックするのがおすすめです。
Array.filter
Array.filterは配列の各要素に対し、順番にメソッド内に記述した callback を実行し、条件に一致した結果を返します。
callback に与えられる引数は以下です。
- 第 1 引数:配列 n 番目の要素の値
- 第 2 引数:第 1 引数の index(省略可)
- 第 3 引数:元の配列(省略可)
返り値は条件に一致した要素からなる新しい配列です。
this 変数も Array.map などと同様に callback に渡せます。
動作を確認していきます。以下、サンプルを確認ください。
const array = ["apple", "banana", "grapes", "mango", "orange"];
const newArray = array.filter((element) => {
return element.indexOf("ap") !== -1;
});
console.log(newArray);
// ['apple', 'grapes']
// mapなどと同様省略記法でもOKです
const newArray = array.filter((element) => element.indexOf("ap") !== -1);
console.log(newArray);
String.indexOfを使用しapの文字列を含む値のみを返すという条件を与えています。
このようにサンプルとそのメソッドの名前からイメージのつく通り Array.filter は配列に対してフィルタをかけた結果を取得したい時などに最適です。
また、Array.filter は条件に一致した要素を配列で返すという仕様です。条件に一致するということはすなわち真偽値で返しても同様の結果を得ることができます。
以下、その一例をサンプルとして載せます。
const array = ["apple", "banana", "grapes", "mango", "orange"];
const newArray = array.filter((element) => {
if (element.indexOf("ap") !== -1) {
return true;
}
return false;
});
console.log(newArray);
// ['apple', 'grapes']
一見true / falseの配列を生成しているように見えますが Array.filter では true を返せば callback 実行中の要素を返し、false ではスキップするという動きになります。
要素を追加、削除した時のサンプルも確認してみます。
// 要素の追加
const array = [1, 2, 3];
const newArray = array.filter((element, index) => {
if (index < 1) array.push(4);
return element <= 4;
});
console.log(newArray);
console.log(array);
/*
[ 1, 2, 3 ]
[ 1, 2, 3, 4 ]
*/
// 要素の削除
const array = [1, 2, 3];
const newArray = array.filter((element, index) => {
if (index < 1) array.splice(2, 1);
return element <= 4;
});
console.log(newArray);
console.log(array);
/*
[ 1, 2 ]
[ 1, 2 ]
*/
追加の場合は Array.map と同様参照しないですが、Array.map の時は削除すると empty が結果として返ってきたの対し、Array.filter では返ってこないという違いがあります。
メソッドチェーンも可能です。以下のサンプルを確認ください。
const weeklyMagazines = [
{ title: "呪術廻戦", author: "芥見下々", publisher: "少年ジャンプ" },
{ title: "ONE PIECE", author: "尾田栄一郎", publisher: "少年ジャンプ" },
{ title: "鬼滅の刃", author: "吾峠呼世晴", publisher: "少年ジャンプ" },
{ title: "キングダム", author: "原泰久", publisher: "ヤングジャンプ" },
{
title: "かぐや様は告らせたい",
author: "赤坂アカ",
publisher: "ヤングジャンプ",
},
];
weeklyMagazines
.filter((magazine) => magazine.publisher === "少年ジャンプ")
.map((jumpComic) => `${jumpComic.title}の作者は${jumpComic.author}`)
.forEach((element) => console.log(element));
/*
呪術廻戦の作者は芥見下々
ONE PIECEの作者は尾田栄一郎
鬼滅の刃の作者は吾峠呼世晴
*/
Array.filter で少年ジャンプ連載のマンガのみにフィルタしてから Array.map で作品名+作者の新しい配列を生成し、Array.forEach で結果を出力しています。
Array.reduce
Array.reduceは配列の各要素に対し、順番にメソッド内に記述した callback を実行します。その際に直前の要素が返されその要素を元に処理した結果が最終値になります。
callback に与えられる引数は以下です。
- 第 1 引数:直前の要素の結果
- 第 2 引数:配列 n 番目の要素の値
- 第 3 引数:第 1 引数の index(省略可)
- 第 4 引数:元の配列(省略可)
返り値は配列全体に実行された callback の結果です。
Array.reduce は Array.map や Array.filter などでいう this の変数を渡す場所でコールバックの初期値を任意で渡すことができます。
この初期値を渡したかどうかで結果が異なるのでそれぞれの結果を見ていきます。
callback に初期値を渡さない場合
Array.reduce は callback の第 1 引数が直前の要素の結果となります。
それでは配列 0 番目というまだ直前の要素が存在しない場合ではどのようなふるまいになるのでしょうか。
以下のサンプルで動作を確認ください。
const array = [1, 2, 3, 4];
const newArray = array.reduce((pre, current, index, self) => {
return current;
});
// 4
// 省略記法でもokです
const newArray = array.reduce((pre, current, index, self) => current);
ただ単に callbalck 実行中の配列の要素を返すだけのサンプルで結果は 4 です。
4 つの引数を全部 console で出力した時の結果をまとめて見ましょう。
pre | current | index | self | return |
---|---|---|---|---|
1 | 2 | 1 | [ 1, 2, 3, 4 ] | 2 |
2 | 3 | 2 | [ 1, 2, 3, 4 ] | 3 |
3 | 4 | 3 | [ 1, 2, 3, 4 ] | 4 |
4 つの要素からなる配列なのに current 値は 2 でループの回数は 3 回、index も 1 からスタートと見た感じ最初の要素がスキップされた挙動に見えます。
pre には最初の要素の値である 1 が入っています。
以上から初期値が指定されなかった場合配列の最初の要素で初期化され、current が 2 番目の要素からはじまるということが確認できます。
次に最初の説明でもふれました直前の要素を返すとはどういうことでしょうか。
以下のサンプルを確認ください。
const array = [1, 2, 3, 4];
const newArray = array.reduce((pre, current, index, self) => {
return pre + current;
});
// 10
// 省略記法でもokです
const newArray = array.reduce((pre, current, index, self) => current);
直前の結果(pre)と現在の要素(current)を足した結果を返すサンプルで結果は 10 です。
先ほどと同様に 4 つの引数を全部 console で出力した時の結果をまとめて見ましょう。
pre | current | index | self | return |
---|---|---|---|---|
1 | 2 | 1 | [ 1, 2, 3, 4 ] | 3 |
3 | 3 | 2 | [ 1, 2, 3, 4 ] | 6 |
6 | 4 | 3 | [ 1, 2, 3, 4 ] | 10 |
pre の値が加算されていますね。加算した値は直前の要素の pre と current を足した return の結果です。直前の要素を返すというのは動きのことを指します。
callback に初期値を渡す場合
次に callback に初期値を渡した場合の挙動です。
まずはサンプルで動作と構文を確認ください。
const array = [1, 2, 3, 4];
const newArray = array.reduce((pre, current, index, self) => {
return pre + current;
}, 10);
console.log(newArray);
// 20
// 省略記法でもかけます
const newArray = array.reduce((pre, current, index, self) => pre + current, 10);
callback の後ろに 10 の値を渡してます。この値が初期値になり出力結果は 20 になります。こちらも同様に結果を整理していきましょう。
pre | current | index | self | return |
---|---|---|---|---|
10 | 1 | 0 | [ 1, 2, 3, 4 ] | 11 |
11 | 2 | 1 | [ 1, 2, 3, 4 ] | 13 |
13 | 3 | 2 | [ 1, 2, 3, 4 ] | 16 |
16 | 4 | 3 | [ 1, 2, 3, 4 ] | 20 |
ご覧の通り初期値を渡すことにより渡された値で初期化され配列の 0 番目から順番に実行されるようになります。
また、初期値で渡す値は数値である必要はありません。この初期値に渡す値によっていろいろな使い方ができるようになります。
一例をサンプルで確認してみます。
使用例:配列の重複をなくす
Array.reduce を使った一例として配列の重複をなくすというコードを書いてみます。
まずは Array.map を使った失敗する例を見てみます。
const array = ["apple", "orange", "peach", "banana", "apple", "banana"];
const newArray = array.map((elem, index, self) => {
if (self.indexOf(elem) === index) {
return elem;
}
});
console.log(newArray);
// [ 'apple', 'orange', 'peach', 'banana', undefined, undefined ]
重複の値は検知できますが undefined が配列に入ってきてしまいます。
それでは、Array.reduce での例を見てみます。
const array = ["apple", "orange", "peach", "banana", "apple", "banana"];
const newArray = array.reduce((prev, current) => {
if (!prev.includes(current)) {
prev.push(current);
}
return prev;
}, []);
console.log(newArray);
// [ 'apple', 'orange', 'peach', 'banana' ]
初期値に空の配列を渡すのと、Array.reduce の直前の結果が返るという性質を利用して直前の配列の中に現在の要素の値が存在しなかった場合は配列に追加するという処理を行っています。
とは言ってもこの例だと Array.filter でも実現できるんですよね。
const array = ["apple", "orange", "peach", "banana", "apple", "banana"];
const newArray = array.filter(
(elem, index, self) => self.indexOf(elem) === index
);
console.log(newArray);
Array.filter の真偽値を返せば要素の値が返る性質を利用してます。省略記法でも書けてこっちのがスマートですね。
Array.reduce は始めはとっつき難いかもしれません。
ただ慣れるとめっちゃ便利で、Array.reduce 使えば大体の配列処理ができてしまいます。
ただ、便利だからってなんでもかんでも使うというのはおすすめではないです。
あくまで、やりたいことに一番近しい名前と機能を持っているメソッドを選択しコードの可読性は可能な限り高くする努力はしましょう。
Array.find
Array.findは配列の各要素に対し、順番にメソッド内に記述した callback を実行し、条件に一致した最初の要素の結果を返します。
callback に与えられる引数は以下です。
- 第 1 引数:配列 n 番目の要素の値
- 第 2 引数:第 1 引数の index(省略可)
- 第 3 引数:元の配列(省略可)
返り値は条件に一致した最初の要素です。条件に一致しなかった場合はundefinedが返ります。
this 変数もここまでと同様に callback に渡せます。
サンプルで動作を確認していきます。
const array = ["apple", "orange", "peach", "banana", "apple", "banana"];
const newArray = array.find((elem) => {
return elem === "banana";
});
console.log(newArray);
// banana
// 省略記法でもOKです
const newArray = array.find((elem) => elem === "banana");
// 配列の要素内に条件値が存在しない場合はundefinedを返す
const newArray = array.find((elem) => elem === "tomato");
console.log(newArray);
// undefined
このように Array.find は言葉から察しがつくように配列内の要素の値を探し、その結果を取得したいというときに最適です。
配列内の要素の値ではなくインデックスを取得したい場合はArray.findIndexを使用します。
その他にも配列内の指定した値のインデックスを取得するArray.indexOfや配列内に要素が存在するか真偽値を取得するArray.includesなどがありますが、これらは配列の検索というカテゴリで改めてどこかでまとめようかなと思います。
話を戻します。Array.find は Array.filter と同様に条件の一致を見ますので真偽値を返すことでも対象の要素を取得することができます。
サンプルを確認ください。
const array = ["apple", "orange", "peach", "banana", "apple", "banana"];
const newArray = array.find((elem) => {
if (elem === "banana") {
return true;
}
return false;
});
console.log(newArray);
// banana
要素が banana の時に true を返していますが出力結果は banana であることを確認できます。
メソッドが呼び出された時に配列に要素を追加・削除した場合の動きも確認しておきます。
// 配列に要素を追加
const array = ["apple", "orange", "peach", "banana", "apple", "banana"];
const newArray = array.find((elem, index) => {
if (index < 1) array.push("tomato");
return elem === "tomato";
});
console.log(newArray);
// undefined
// 配列を削除した場合
const array = ["apple", "orange", "peach", "banana", "apple", "banana"];
const newArray = array.find((elem, index) => {
if (index < 1) array.splice(0, 3);
console.log(elem);
/*
apple
apple
banana
undefined
undefined
undefined
*/
return elem === "peach";
});
console.log(newArray);
// undefined
途中で追加した要素に対しては callback が実行されず結果は undefined です。
削除の場合、最初に先頭から 3 つの要素を削除しました。
削除されていない要素に対して callback が実行された後、削除された要素が undefined として残り callback が実行されていることが確認できます。
一致条件は peach でその要素はすでに削除しているので結果は undefined ですが、Array.find では削除された要素に対しても callback が適用されるという一例です。
Array.every
Array.everyは配列の各要素に対し、順番にメソッド内に記述した callback を実行し、要素の全てが条件に一致したかの結果を返します。
callback に与えられる引数は以下です。
- 第 1 引数:配列 n 番目の要素の値
- 第 2 引数:第 1 引数の index(省略可)
- 第 3 引数:元の配列(省略可)
返り値はtrue / false の真偽値です。
this 変数もここまでと同様に callback に渡せます。
まずはサンプルで動作を基本動作の確認をしてみます。以下をご確認ください。
const array = [0, 1, 2, 3, 4, 5];
const newArray = array.every((elem) => {
return elem <= 5;
});
console.log(newArray);
// true
// 省略記法でもOK
const newArray = array.every((elem) => elem <= 5);
配列の各要素の値が 5 以下がどうかの条件で検証し全て条件に一致したので true を返す例です。
では次の要素をそのまま返している例では結果を何で返すでしょうか。
const array = [0, 1, 2, 3, 4, 5];
const newArray = array.every((elem) => elem);
console.log(newArray);
// ?????
答えは false を返します。
これは JavaScript では 0 は false とみなすという仕様のためです。その他にも値のみで false と判定するリテラル値は複数ありますので簡単にリスト化しておきます。
リテラル | 判定結果 |
---|---|
{} | true |
"文字列" | true |
"" | false |
1 | true |
-1 | true |
0 | false |
[] | true |
true | true |
false | false |
undefined | false |
null | false |
NaN | false |
以上をふまえ次のサンプルを確認ください。
const array = [0, "", undefined, null];
const newArray = array.every((elem) => elem);
console.log(newArray);
// false
// 反転すればtrueになる
const newArray = array.every((elem) => !elem);
console.log(newArray);
// true
リストを元に false と判定するリテラルのみで構成した配列で検証した結果 false を返し、エクスクラメーション(!)で否定すれば true が返るサンプルです。
この特性を生かすことで配列の正当性などを検証するのに Array.every は最適です。
また、判定式のリストはそのまま if の条件などにも当てはまりますので覚えておくと良いと思います。
次に配列を追加・削除した場合のサンプルを確認していきましょう。以下をご確認ください。
// 配列に要素追加
const array = [0, 1, 2, 3, 4, 5];
const newArray = array.every((elem, index) => {
if (index < 1) array.push(6);
console.log(elem);
/*
0
1
2
3
4
5
*/
return elem <= 5;
});
console.log(newArray);
// true
// 配列の要素を削除
const array = [0, 1, 2, 3, 4, 5];
const newArray = array.every((elem, index) => {
if (index < 1) array.splice(2, 4);
console.log(elem);
/*
0
1
*/
return elem <= 5;
});
console.log(newArray);
// true
配列への追加および削除した要素に対して処理を行うことはないことが確認できます。
また、配列として不完全な疎配列に対しても Array.every は参照することはありません。
const array = [0, 1, , , , 5];
const newArray = array.every((elem, index) => {
console.log(elem);
return elem <= 5;
/*
0
1
5
*/
});
console.log(newArray);
// true
処理が飛ばされていることが確認できますね。
条件の判定なので Array.filter や Array.find と同様に真偽値を明示的に返すやり方でももちろんオッケーです。
// trueが返る
const array = [0, 1, 2, 3, 4, 5];
const newArray = array.every((elem, index) => {
if (elem <= 5) {
return true;
}
return false;
});
console.log(newArray);
// true
// falseが返る
const array = [0, 1, 2, 3, 4, 5, 6];
const newArray = array.every((elem, index) => {
if (elem <= 5) {
return true;
}
return false;
});
console.log(newArray);
// false
条件の値を変更してみても期待通りの結果になってますね。
Array.some
Array.someは配列の各要素に対し、順番にメソッド内に記述した callback を実行し、条件が要素のどれかしらに一致したかの結果を返します。
callback に与えられる引数は以下です。
- 第 1 引数:配列 n 番目の要素の値
- 第 2 引数:第 1 引数の index(省略可)
- 第 3 引数:元の配列(省略可)
返り値はtrue / false の真偽値です。
this 変数もここまでと同様に callback に渡せます。
動作の特徴としては Array.every と一緒ですのでざっと確認してみましょう。
// 5より大きい要素が1つでも存在するか
const array = [0, 1, 2, 3, 4, 5, 99];
const newArray = array.some((elem) => {
return elem > 5;
});
console.log(newArray);
// true
// 省略記法でもOK
const newArray = array.some((elem) => elem > 5);
// false判定ではない要素が1つでも存在するか
const array = [0, "", undefined, null, "string"];
console.log(newArray);
// true
const array = [0, "", undefined, null];
console.log(newArray);
// false
// 要素を追加
const array = [0, 1, 2, 3, 4, 5];
const newArray = array.some((elem, index) => {
if (index < 1) array.push(99);
console.log(elem);
/*
0
1
2
3
4
5
*/
return elem > 5;
});
console.log(newArray);
// false
// 要素の削除
const array = [0, 1, 2, 3, 4, 5, 99];
const newArray = array.some((elem, index) => {
if (index < 1) array.splice(3, 4);
console.log(elem);
/*
0
1
2
*/
return elem > 5;
});
console.log(newArray);
// false
動きは同じですのでArray.every は全要素が条件一致、Array.some はどれかしらの要素が条件一致ということを頭に入れておきましょう。
さいごに
さいごまで読んでいただきありがとうございます。
まだまだ配列系のメソッドはありますが、よく使うものとしては大体網羅できたかと思います。
ちょっとしたクイックリファレンスみたいに活用してもらえたりしたら嬉しいです。
余談ですが、プロジェクトによっては forEach や for の使用は禁止。基本繰り返し処理は map を使いましょう。みたいなコーディング規約があったりします。
個人的には記事内でちょくちょく言及してますが、何をしたいかによって、その名前や機能から一番可読性が高くなる方法を選ぶのがベストであると思うので反対派です。
コード規約ならそれに沿うが最適解ではあるのですが、何でもかんでもモダンな Array メソッドを使えばいいってことでもないというのはスタンスとして持っておくと良いのではないかなと思い書いた余談でした。
間違いの指摘やリクエストなどありましたら加筆していきたので是非、ご意見をいただけたらと思います。
それではまた次の記事でお会いしましょう。
Discussion