9.

JavaScript のループ処理|for...of と forEach・await が効かない理由

編集
この記事の要点
  • 配列を回すなら for...of。番号が要るなら entries() と組み合わせる
  • for...in は配列に使わない。キーが文字列で来て、継承したプロパティも拾う
  • forEach の中では await が効かず、break もできない
  • ループ変数は letvar だと非同期処理で全部同じ値になる
  • 「作って返すだけ」のループは map / filter / reduce に置き換えると意図が伝わる

ループの種類

書き方取り出すものbreakawait
for (let i = 0; ...)添字
for...of
for...inキー(文字列)
forEach値・添字・配列××
while / do...while
const fruits = ["りんご", "みかん", "ぶどう"];

for (const f of fruits) console.log(f);                 // 値

for (const [i, f] of fruits.entries()) console.log(i, f); // 番号 + 値

fruits.forEach((f, i) => console.log(i, f));

for (let i = 0; i < fruits.length; i++) console.log(fruits[i]);

let n = 3;
while (n > 0) { console.log(n); n--; }

do { console.log("1 回は必ず実行"); } while (false);

for...in を配列に使わない

const arr = [10, 20, 30];

for (const i of arr) console.log(i);      // 10 20 30   値
for (const i in arr) console.log(i);      // "0" "1" "2"   キー(文字列!)

for (const i in arr) {
  console.log(arr[i] + 1);                // 11 21 31(たまたま動く)
  console.log(i + 1);                     // "01" "11" "21"  ← 文字列連結になる
}

// 誰かが配列を拡張していると余計なものまで拾う
Array.prototype.custom = () => {};
for (const k in arr) console.log(k);      // "0" "1" "2" "custom"
for (const v of arr) console.log(v);      // 10 20 30(影響なし)

for...in はオブジェクトのキーを回すための構文です。配列には for...of を使ってください。

オブジェクトを回す

const scores = { math: 80, english: 70 };

// 推奨: entries で分解する
for (const [key, value] of Object.entries(scores)) {
  console.log(`${key}: ${value}`);
}

// for...in を使うなら自分のキーだけに絞る
for (const key in scores) {
  if (!Object.hasOwn(scores, key)) continue;
  console.log(key, scores[key]);
}

// Map はそのまま回せる
const m = new Map([["a", 1]]);
for (const [k, v] of m) console.log(k, v);

forEach の制約

const nums = [1, 2, 3, 4];

// break できない
nums.forEach((n) => {
  if (n > 2) return;      // この 1 回を抜けるだけ(continue 相当)
  console.log(n);
});

// 途中で止めたいなら for...of か some を使う
for (const n of nums) {
  if (n > 2) break;
  console.log(n);
}
nums.some((n) => { if (n > 2) return true; console.log(n); });

await が効かない

// 期待どおりに動かない: forEach は Promise を待たない
ids.forEach(async (id) => {
  const user = await fetchUser(id);
  console.log(user);
});
console.log("完了");        // ← 取得を待たずにここへ来る

// 順番に処理する
for (const id of ids) {
  const user = await fetchUser(id);
  console.log(user);
}
console.log("完了");

// 並行して処理する(速いが同時接続に注意)
const users = await Promise.all(ids.map((id) => fetchUser(id)));

// 1 件失敗しても他を続けたい
const results = await Promise.allSettled(ids.map((id) => fetchUser(id)));

非同期処理を含むループでは forEach を使わないのが鉄則です。詳細は 非同期処理 を参照してください。

break と continue

for (const n of [1, 2, 3, 4, 5]) {
  if (n % 2 === 0) continue;   // 偶数は飛ばす
  if (n > 3) break;            // 3 を超えたら抜ける
  console.log(n);              // 1 3
}

// 多重ループはラベルで一気に抜けられる
outer:
for (const row of matrix) {
  for (const v of row) {
    if (v === target) break outer;
  }
}

// 関数にして return するほうが読みやすい
function find(matrix, target) {
  for (const [y, row] of matrix.entries()) {
    for (const [x, v] of row.entries()) {
      if (v === target) return [y, x];
    }
  }
  return null;
}

ループ変数と非同期

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3 3 3   ← i が 1 つしかない

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2   ← 繰り返しごとに新しい i

イベントハンドラーを繰り返し登録するときも同じです。ループ変数は必ず let にしてください(変数の宣言)。

ループ中に配列を変更しない

const nums = [1, 2, 3, 4];

// 危険: 削除すると後ろがずれて飛ばされる
for (let i = 0; i < nums.length; i++) {
  if (nums[i] % 2 === 0) nums.splice(i, 1);
}

// 対処 1: 新しい配列を作る(推奨)
const odds = [1, 2, 3, 4].filter((n) => n % 2 !== 0);

// 対処 2: 後ろから回す
for (let i = nums.length - 1; i >= 0; i--) {
  if (nums[i] % 2 === 0) nums.splice(i, 1);
}

DOM を扱うとき

// querySelectorAll は NodeList。for...of と forEach は使えるが map は使えない
for (const li of document.querySelectorAll("li")) li.classList.add("done");

// map などを使いたければ配列にする
const texts = [...document.querySelectorAll("li")].map((li) => li.textContent);

// getElementsByClassName は「生きた」コレクション
const items = document.getElementsByClassName("item");
for (let i = 0; i < items.length; i++) {
  items[i].classList.remove("item");   // 消すたびに length が減り、半分しか処理されない
}
// 対処: 先に配列へ固定する
for (const el of [...items]) el.classList.remove("item");

getElementsByClassName / getElementsByTagName の戻り値はDOM の変更に追従して中身が変わります。ループ内で該当条件を変える場合は、先にスプレッドで配列に固定してください。

ループを書かずに済ませる

const nums = [1, 2, 3, 4, 5];

// 変換するだけ
const doubled = nums.map((n) => n * 2);

// 絞り込むだけ
const evens = nums.filter((n) => n % 2 === 0);

// 集計するだけ
const total = nums.reduce((acc, n) => acc + n, 0);

// 決まった回数だけ回す
Array.from({ length: 3 }, (_, i) => i).forEach((i) => console.log(i));

「配列を作るためだけの for」は map / filter に置き換えられます。副作用(DOM 操作・通信)を伴う処理は素直に for...of で書いてください。

関連

編集
Post Share
子ページ
  1. for文
同階層のページ
  1. 記述方法
  2. コメント
  3. 変数の宣言
  4. 関数
  5. 演算子
  6. 条件文
  7. 配列
  8. 連想配列
  9. ループ処理
  10. 非同期処理
  11. 同期処理
  12. 確認ウィンドウを表示する方法
  13. 文字の置換
  14. base urlを取得する方法
  15. formのsubmit前にjavascriptを呼び出す方法
  16. undefinedのイコール判定
  17. Javascript のみで form を post で submit する方法

最近更新/作成されたページ