LINDORのAdvent Calendar(本物)の16日目を開けたところ。
配列とかおれおれAdvent Calendar2018 – 16日目

繰り返しにもいろいろあるよ。

お待たせ! 現代的JavaScriptでは主流の、よく for を置き換え使うやつです。

まず for の例

const arr = [11, 22, 33];

for (let i = 0; i < arr.length; i++) {
  const value = arr[i];
  console.log(value);
}

forEach() にした例

const arr = [11, 22, 33];

arr.forEach((value) => {
  console.log(value);
});

かんたーん。

簡単なので、あまり語ることはありません。

仕様

引数は他のこれ系の配列メソッドと同じです。

forEach() は引数に、関数オブジェクトをひとつ受け取ります。 戻り値はありません。 (undefined)

与える関数は、3つの引数が与えられます。

  • value … 配列の要素
  • index … インデックス
  • array … 操作中の配列本体
const arr = [11, 22, 33];

arr.forEach((value, index, array) => {
  console.log(index, ':', value, ' <- ', array[index], array);
});

第2引数

実は forEach() その他にはには第2引数 thisArg があります。

これは第1引数の関数実行時に this へ束縛されるオブジェクトなんだけど、 近年は this が変わらないアロー関数を用いるのが主流なので、 使う場面は少ないかなと思います。

const obj = {
  arr: [11, 22, 33],

  logAll: function () {
    this.arr.forEach(function (value) {
      this.output(value);
    }, this);
  },

  output: function (msg) {
    console.log('Message: ', msg);
  },
};

obj.logAll();

再利用

これも他の配列メソッドと同様なんだけど、配列以外のオブジェクトへ適用しても適切に動作するよう設計されています。

基本的に forEach() でできて for でできないことってないんじゃないかな。

普通に繰り返す

const arr = [11, 22, 33];
arr.forEach((value) => {
  console.log(value);
});

配列内の他の要素を参照する

えーと例えば毎年の人口とか、そういう数値情報が与えられて、その増減を見ていきたい場合。

まずは for 文でやる例。

const arr = [100, 110, 115, 103, 110, 90];

for (let i = 1; i < arr.length; i++) {
  const item1 = arr[i - 1];
  const item2 = arr[i];
  const diff = item2 - item1;
  const sign = diff < 0 ? '' : '+';
  console.log(`${item1} -> ${item2} (${sign}${diff})`);
}

// 100 -> 110 (+10)
// 110 -> 115 (+5)
// 115 -> 103 (-12)
// 103 -> 110 (+7)
// 110 -> 90 (-20)

まず最初に i = 1 から始めるのはできないので、繰り返しの中で飛ばします。(slice() とかするとインデックスがずれちゃうので注意。そっちの方が良いかもだけど。)

また配列全体は与える関数の第3引数にもらえるので、これを利用します。

const arr = [100, 110, 115, 103, 110, 90];

arr.forEach((item2, i, all) => {
  if (i < 1) { return; }
  const item1 = all[i - 1];
  const diff = item2 - item1;
  const sign = diff < 0 ? '' : '+';
  console.log(`${item1} -> ${item2} (${sign}${diff})`);
});

ここで all は外側の arr と同じなので、そっちでも良いです。

結果を配列にしたいなら reduce() も有用。

同じ処理を繰り返す

配列記法から直接メソッド実行できるので、匿名関数の即時実行みたいなノリで、匿名配列の即時利用てな感じでも使えます。

[
  '.target1',
  '.target2',
  '.target3',
].forEach((selector) => {
  const el = document.querySelector(selector);
  el.classList.add('targeted');
});

同じ処理をまとめるってなら関数化が正解だと思うんだけど、手軽に書きたいときとか。

セミコロンを行末に置かないスタイルの場合はご注意ください。配列記法 [] が前の行と繋がってしまうので。

似た計算をする

縦横両方向の計算とか、プロパティ名は異なるが算出方法は同じ、みたいな場面で。

// 抜粋

[
  ['clientWidth', 'left'],
  ['clientHeight', 'top'],
].forEach(([sizeName, axisName]) => {
  const pos = (elWrapper[sizeName] - elItem[sizeName]) / 2;
  elItem.style[axisName] = `${pos}px`;
});

純粋な計算と副作用を分けたい場合は map() でも。

forEach() でできないこと

と、他と組み合わせてのやり方。

forEach() は「先頭から末尾まで繰り返す」ものなので、それ以外のパターンで繰り返したい場合はコードこねこねしてやる必要があります。

先頭以外から、末尾以外までの繰り返し

あんまりやらない気もするけど、途中からとか途中までとかはできません。

split() と組み合わせます。

const arr = ['A', 'B', 'C'];

// 先頭から1個までを飛ばす
// => B, C
arr.slice(1).forEach((item, i) => {
  console.log(`${i}: ${item}`);
});

// 末尾から1個までを飛ばす
// => A, B
arr.slice(0, -1).forEach((item, i) => {
  console.log(`${i}: ${item}`);
});

index を見てコールバック関数で return するのでもアリ。

飛ばして回す

一つ飛ばしとかはできません。

できないので、繰り返しのコールバック関数で都度 return して、何もしないようにします。

const arr = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];

// 3つごと
// => A, D, G, J, ...
arr.forEach((c, i) => {
  if (i % 3 !== 0) { return; }
  console.log(`${i}: ${c}`);
});

あるいは事前に弾いておく。こっちの方が見た目はきれいっぽい。

const arr = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];

// 3つごと
// => A, D, G, J, ...
arr
  .filter((_, i) => i % 3 === 0)
  .forEach((c, i) => console.log(`${i}: ${c}`));

逆順に繰り返し

意外とこれができません。常に先頭からです。

配列を逆順にする reverse() というのもあるんだけど、こいつは対象の配列自体を逆順にする破壊的な副作用があるので、利用可能な場面が限定的です。

const arr = ['A', 'B', 'C'];

arr.reverse().forEach((item, i) => {
  console.log(`${i}: ${item}`);
});
// => C, B, A

// 元の配列が変わっちゃう!
console.log(arr); // => [ 'C', 'B', 'A' ]

事前に複製してから reverse() すればいいんだけど。

[...arr].reverse()

うーん、 reduceRight() が良いかな。

const arr = ['A', 'B', 'C'];

arr.reduceRight((_, item, i) => {
  console.log(`${i}: ${item}`);
}, 0);

これはこれで第2引数に何か与えるのと、コールバック関数の第1引数も value でないものが与えられるのを忘れないように気を付けないといけない。

回ってくる値を無視する?

const arr = ['A', 'B', 'C'];

arr.forEach((_, i) => {
  const item = arr[arr.length - 1 - i];
  console.log(`${i}: ${item}`);
});

sort() しちゃうのが意味が明瞭で一番良いかもしれない。

配列以外の forEach()

普通のオブジェクト

配列へ変換してやります。

プロパティ名だけ得る例。

const obj = {
  a: 11,
  b: 22,
  c: 33,
};

Object.keys(obj).forEach((key) => {
  console.log(key);
});
// => a, b, c

他に値だけを得る Object.values() と、両方を得る Object.entries() があります。

for-of でやったやつら。

Map, Set

こいつらは forEach() を持ってます。だいたい同じ動き。

Map はインデックスの代わりにキーが得られます。 ですよねー。

順序ははキーを追加した順。

const map = new Map([
  ['foo', 11],
  ['bar', 22],
  ['boo', 33],
]);
map.forEach((value, key) => {
  console.log(key, value);
});

Set の場合、インデックスやキーとなる部分にも値が与えられます。 わお。

こちらも順序は追加順です。

const set = new Set([11, 22, 33]);
set.forEach((value, v2) => {
  console.log(value, v2, value === v2); // 11, 11, true 等
});

DOM系の配列風オブジェクト

でも使えたりします。

const els = document.querySelectorAll('.target');
console.log(els instanceof Array); // => false
els.forEach((el, index) => {
  console.log(index, el);
});

forEach() 以外の、例えば map() とかはないです。

中身は完全に配列のそれと同じ。そこら辺のもうちょい詳しい話を別稿に用意しました。

jQuery

には each() というのがあったり。

const $els = $('.target');
$els.each((index, el) => {
  console.log(index, el);
});

引数の順序が違う点に注意。 (今時使うのかはわからないけど。)

jQueryオブジェクトは反復可能なので、 [...$els].forEach(fn) も使えます。

for と速度面の比較

まず結論ですが、高速化のために forEach() を避けて for を採用する必要はありません

可読性や記述の用意さから考えても forEach() の方が良いように思います。(個人の感想です。)

遅いかと言われれば、もちろん遅いんだけど、そこの遅さが問題になる状況は普通、既に破綻してますから。それよりも前に気にするべき個所があるはずです。計算量( O(n2) とかそういうやつ)を考えて、ファイルアクセスやら画面再描画やらの遅いAPIに気を付けて。

2012年の実験があります。0.001ミリ秒以下の差です。0.001秒じゃないよ。

よっぽど極まった場面では別だけど、まあ微妙な速度性能差ではなく機能差で選ぼうね。

あ、でも

何か探すとかで最後まで繰り返す必要がない場合は forEach() じゃなくて find() とか some() とか、そういう適切なものを使おう。意味が明瞭になって可読性も向上するし。

おしまい

繰り返しシリーズおしまい。はー長かった。

forEach() は便利で大変よろしい。

関連

参考