JavaScriptの基礎 - 反復処理(for文)

提供: MochiuWiki : SUSE, EC, PCB

概要

JavaScriptにおける反復処理 (ループ) の中心となるのは、forfor...offor...in の3種類の構文である。

for 文はJavaScript誕生当初から存在する基本的なループ構文であり、カウンタ変数を使って任意の回数だけ処理を繰り返す。
インデックスを直接操作できるため、配列要素の位置に依存した処理や、複数の配列を並行して操作する場合に適している。

for...of 文はES2015 (ES6) で導入された構文であり、イテラブルプロトコルに対応するオブジェクト (配列、文字列、Map、Set、NodeList等) の要素を順番に取り出して反復する。
コードが簡潔になり、値の取得に集中できるため、現代的なJavaScript開発での配列やコレクション操作において推奨される。

for...in 文はオブジェクトの列挙可能なプロパティキーを反復する構文である。
プロトタイプチェーンの影響を受けるため、使用には注意が必要であり、配列の反復には使用してはならない。

3種類のループを適切に使い分けることが、可読性の高い堅牢なソースコードを記述する上で重要である。

原則として、配列や文字列の反復には for...of、インデックス操作が必要な場合は for
オブジェクトのプロパティを列挙する場合は Object.entries()for...of の組み合わせを使用することが推奨される。


for文

for 文は、初期化式、条件式、更新式の3つの部分で構成されるループ構文である。
ループの制御を細かく記述できるため、カウンタを使った反復処理に適している。

基本構文

for 文の基本的な構文を以下に示す。

 for (initialization; condition; afterthought) {
    statement
 }


各部分の役割を以下に示す。

for 文の構成要素
要素 説明
initialization (初期化式) ループ開始前に1度だけ実行される。
通常はカウンタ変数の宣言と初期化を行う。
condition (条件式) 各反復の前に評価される。
trueであればループ本体を実行し、falseになるとループを終了する。
afterthought (更新式) 各反復の後に実行される。
通常はカウンタ変数のインクリメントやデクリメントを行う。


基本的な使用例を以下に示す。

 // 0〜4まで出力
 for (let i = 0; i < 5; i++) {
    console.log(i); // 0, 1, 2, 3, 4
 }
 
 // 逆順に反復
 for (let i = 4; i >= 0; i--) {
    console.log(i); // 4, 3, 2, 1, 0
 }
 
 // 2ずつインクリメント
 for (let i = 0; i <= 10; i += 2) {
    console.log(i); // 0, 2, 4, 6, 8, 10
 }


初期化式と更新式は省略することができる。

 let i = 0;
 for (; i < 3;) {
    console.log(i);
    i++;
 }


ループカウンタ

for 文のカウンタ変数には、let を使用することが推奨される。
var を使用するとクロージャ問題が発生するため、現代的なJavaScript開発では使用しない。

  • var を使用した場合のクロージャ問題
     // var を使用した場合: クロージャ問題が発生する
     var funcs = [];
     for (var i = 0; i < 3; i++) {
        funcs.push(function() {
           console.log(i);
        });
     }
     
     funcs[0]();  // 3 (期待値: 0)
     funcs[1]();  // 3 (期待値: 1)
     funcs[2]();  // 3 (期待値: 2)
     // varはブロックスコープを持たないため、全ての関数が同じ変数iを参照する
    

  • let を使用して問題を解決する例
    let はブロックスコープを持つため、各反復で独立した変数が作成される。
    このため、非同期処理やコールバック関数を扱う場面でも正しく動作する。
     // let を使用した場合: 各反復で独立したスコープが作成される
     let funcs2 = [];
     for (let i = 0; i < 3; i++) {
        funcs2.push(function() {
           console.log(i);
        });
     }
     
     funcs2[0]();  // 0
     funcs2[1]();  // 1
     funcs2[2]();  // 2
    


配列の反復

for 文で配列を反復する場合、インデックスを使って各要素にアクセスする。

 const arr = [10, 20, 30, 40, 50];
 
 // 基本的な配列の反復
 for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]); // 10, 20, 30, 40, 50
 }


大きな配列を反復する場合、length プロパティへのアクセスを毎回行うとパフォーマンスに影響することがある。
length の値をキャッシュすることで、この影響を軽減できる。

 // length をキャッシュしてパフォーマンスを最適化する
 const arr = [10, 20, 30, 40, 50];
 for (let i = 0, len = arr.length; i < len; i++) {
    console.log(arr[i]);
 }


インデックスと値の両方を使用する例を以下に示す。

 const fruits = ["apple", "banana", "cherry"];
 
 for (let i = 0; i < fruits.length; i++) {
    console.log(`[${i}] ${fruits[i]}`);
    // [0] apple
    // [1] banana
    // [2] cherry
 }


ネストしたfor文

for 文はネストして使用することができる。
2次元配列の処理や、総当たりの組み合わせを求める場合等に活用される。

 // 2次元配列の反復
 const matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
 ];
 
 for (let i = 0; i < matrix.length; i++) {
    for (let j = 0; j < matrix[i].length; j++) {
       console.log(matrix[i][j]); // 1, 2, 3, 4, 5, 6, 7, 8, 9
    }
 }


ネストしたループでは、通常の break は内側のループのみを抜け出す。
外側のループも含めて1度に抜け出すには、ラベル付きbreak を使用する。

 const matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
 ];
 const target = 5;
 
 // ラベル付きbreakで外側のループを抜ける
 outerLoop: for (let i = 0; i < matrix.length; i++) {
    for (let j = 0; j < matrix[i].length; j++) {
       if (matrix[i][j] === target) {
          console.log(`Found at [${i}, ${j}]`); // Found at [1, 1]
          break outerLoop; // 外側のループごと終了する
       }
    }
 }


ラベル付き continue を使用すると、外側のループの次の反復にジャンプすることもできる。

 outer: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
       if (j === 1) continue outer; // 外側のループの次の反復へ
       console.log(`i=${i}, j=${j}`);
       // i=0, j=0
       // i=1, j=0
       // i=2, j=0
    }
 }


無限ループとfor文

for 文の3つの式を全て省略すると、条件無しの無限ループになる。
無限ループは、終了条件を break で制御する場合に使用する。

 // for (;;) は無限ループを表す慣用的な記法
 let count = 0;
 for (;;) {
    count++;
    if (count >= 5) break; // 終了条件で break する
 }

 console.log(count); // 5


無限ループは、ゲームループやサーバのイベントループ等、明示的な終了条件が必要な場面で使用される。
ただし、break による終了条件を必ず設けなければ、プログラムが停止しなくなるため注意が必要である。


for...of文

for...of 文は、ES2015 (ES6)で導入されたループ構文であり、イテラブルプロトコルに対応するオブジェクトの要素を順番に取り出して反復する。

基本構文

for...of 文の基本的な構文を以下に示す。

 for (variable of iterable) {
    statement
 }


variable には各反復で取り出された値が代入される。
iterable はイテラブルプロトコルに対応するオブジェクトであり、内部的に Symbol.iterator メソッドを実装する。

for...of で使用可能な主なイテラブルを以下に示す。

  • 配列 (Array)
  • 文字列 (String)
  • Map
  • Set
  • NodeList
  • arguments オブジェクト
  • ジェネレータオブジェクト


基本的な使用例を以下に示す。

 const colors = ["red", "green", "blue"];
 
 for (const color of colors) {
    console.log(color); // red, green, blue
 }


各反復で値を変更しない場合は const を使用することが推奨される。
反復中に値を変更する必要がある場合は let を使用する。

配列の反復

配列の反復に for...of を使用すると、インデックスを気にせず各要素に直接アクセスできる。

 const numbers = [10, 20, 30, 40, 50];
 
 // 値のみを取得
 for (const num of numbers) {
    console.log(num); // 10, 20, 30, 40, 50
 }


インデックスと値の両方が必要な場合は、配列の entries() メソッドと組み合わせて使用する。

 const fruits = ["apple", "banana", "cherry"];
 
 // entries() でインデックスと値の両方を取得
 for (const [index, fruit] of fruits.entries()) {
    console.log(`[${index}] ${fruit}`);
    // [0] apple
    // [1] banana
    // [2] cherry
 }


インデックスのみが必要な場合は keys()、値のみが必要な場合は values() を使用できる。

 const arr = ["a", "b", "c"];
 
 // keys(): インデックスを反復
 for (const index of arr.keys()) {
    console.log(index); // 0, 1, 2
 }
 
 // values(): 値を反復 (for...of と同等)
 for (const value of arr.values()) {
    console.log(value); // a, b, c
 }


文字列の反復

for...of は文字列を UNICODEコードポイント単位で反復する。
サロゲートペアで表現される文字 (絵文字や一部の漢字など) も1文字として正しく扱われる点が、従来のfor文との大きな違いである。

 // for...of: Unicodeコードポイント単位で反復 (サロゲートペアに対応)
 const emoji = "Hello 😀";
 
 for (const char of emoji) {
    console.log(char);
    // H, e, l, l, o, ' ', 😀 (7回の反復)
    // 😀 が 1文字として正しく扱われる
 }


従来のfor文では、サロゲートペアが2つのコード単位に分割されてしまう。

 // 従来の for 文: サロゲートペアが分割される
 const emoji = "Hello 😀";
 
 for (let i = 0; i < emoji.length; i++) {
    console.log(emoji[i]);
    // H, e, l, l, o, ' ', (サロゲートペアの前半), (サロゲートペアの後半)
    // 😀 が 2文字に分割される (8回の反復)
 }


UNICODE文字を正しく扱う必要がある場面では、for...of を使用することを推奨する。

Mapの反復

Map オブジェクトは、for...of で反復することができる。
各反復では、[key, value] のペアが取り出される。

 const map = new Map([
    ["name", "Taro"],
    ["age", 30],
    ["city", "Tokyo"]
 ]);
 
 // [key, value] のペアを反復
 for (const [key, value] of map) {
    console.log(`${key}: ${value}`);
    // name: Taro
    // age: 30
    // city: Tokyo
 }


Mapkeys()values()entries() メソッドも for...of で使用できる。

 const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
 
 // キーのみを反復
 for (const key of map.keys()) {
    console.log(key); // a, b, c
 }
 
 // 値のみを反復
 for (const value of map.values()) {
    console.log(value); // 1, 2, 3
 }


Setの反復

Set オブジェクトは重複しない値のコレクションであり、for...of で反復できる。
挿入順に値が取り出される。

 const set = new Set([1, 2, 3, 2, 1]); // 重複は自動的に除去される
 
 for (const value of set) {
    console.log(value); // 1, 2, 3
 }


文字列の重複除去と反復を組み合わせる実用的な例を以下に示す。

 const words = ["apple", "banana", "apple", "cherry", "banana"];
 const uniqueWords = new Set(words);
 
 for (const word of uniqueWords) {
    console.log(word); // apple, banana, cherry
 }


NodeListの反復

Webブラウザ環境では、document.querySelectorAll() 等が返す NodeListfor...of で反復できる。

 // 全ての <p> 要素を取得して反復
 const paragraphs = document.querySelectorAll("p");
 
 for (const paragraph of paragraphs) {
    paragraph.style.color = "blue";
    console.log(paragraph.textContent);
 }


NodeList は配列ではないため、Array.prototype のメソッドを直接使用できない場合がある。
for...ofNodeList のイテラブルインターフェースを通じて動作するため、配列への変換が不要になる。


for...in文

for...in 文は、オブジェクトの列挙可能な (enumerable) プロパティキーを反復する構文である。
プロトタイプチェーンの影響を受けるため、使用する場面を慎重に選ぶ必要がある。

基本構文

for...in 文の基本的な構文を以下に示す。

 for (variable in object) {
    statement
 }


variable には各反復でプロパティキーが文字列として代入される。

基本的な使用例を以下に示す。

 const person = {
    name: "Taro",
    age: 30,
    city: "Tokyo"
 };
 
 for (const key in person) {
    console.log(`${key}: ${person[key]}`);
    // name: Taro
    // age: 30
    // city: Tokyo
 }


プロトタイプチェーンの影響

for...in は、オブジェクト自身のプロパティだけでなく、プロトタイプチェーンを通じて継承されたプロパティも列挙する。

 function Parent() {
    this.a = 1;
 }
 Parent.prototype.b = 2; // プロトタイプに追加
 
 const obj = new Parent();
 
 for (const key in obj) {
    console.log(key); // "a", "b" (b は継承プロパティ)
 }


継承されたプロパティを除外して、オブジェクト自身のプロパティのみを処理するには、Object.hasOwn() (ES2022) または hasOwnProperty() を使用する。

 function Parent() {
    this.a = 1;
 }
 Parent.prototype.b = 2;
 
 const obj = new Parent();
 
 // Object.hasOwn を使用 (ES2022以降で推奨)
 for (const key in obj) {
    if (Object.hasOwn(obj, key)) {
       console.log(key); // "a" のみ
    }
 }
 
 // hasOwnProperty を使用 (互換性が必要な場合)
 for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
       console.log(key); // "a" のみ
    }
 }


Object.hasOwn()hasOwnProperty() よりも安全であり、hasOwnProperty が上書きされたオブジェクトでも正しく動作する。
現代的なコードでは Object.hasOwn() の使用が推奨される。

配列にfor...inを使用してはならない理由

for...in を配列の反復に使用してはならない。
配列に for...in を使用することにより、以下の問題が発生する。

  • インデックスが文字列になる。
    配列のインデックスは数値だが、for...in ではキーとして文字列 ("0", "1", "2") が返される。
    数値計算に使用すると、型変換による予期しない動作が発生する可能性がある。
  • プロトタイプに追加されたプロパティが列挙される。
    Array.prototype に追加されたメソッドや、配列に直接追加されたプロパティも列挙対象になる。
  • 列挙順序が保証されない場合がある。
    配列のインデックス順での反復が保証されず、エンジンの実装によって異なる可能性がある。


問題を示すコード例を以下に示す。

 const arr = [10, 20, 30];
 arr.custom = "extra"; // 配列に直接プロパティを追加
 
 for (const i in arr) {
    console.log(i);
    // "0", "1", "2", "custom"
    // custom が意図せず列挙される
    // インデックスは文字列として返される
 }


配列の反復には for 文 または for...of 文を使用すること。

Object.keys / Object.values / Object.entries

for...in の代わりに、Object.keys()Object.values()Object.entries()for...of を組み合わせる方法が推奨される。
これらのメソッドはプロトタイプチェーンの影響を受けず、オブジェクト自身の列挙可能なプロパティのみを対象とする。

 const obj = { name: "Taro", age: 30, city: "Tokyo" };
 
 // Object.keys(): プロパティキーの配列を返す
 console.log(Object.keys(obj));   // ["name", "age", "city"]
 
 for (const key of Object.keys(obj)) {
    console.log(key); // name, age, city
 }


 const obj = { name: "Taro", age: 30, city: "Tokyo" };
 
 // Object.values(): プロパティ値の配列を返す
 console.log(Object.values(obj)); // ["Taro", 30, "Tokyo"]
 
 for (const value of Object.values(obj)) {
    console.log(value); // Taro, 30, Tokyo
 }


 const obj = { name: "Taro", age: 30, city: "Tokyo" };
 
 // Object.entries(): [key, value] ペアの配列を返す
 console.log(Object.entries(obj));
 // [["name", "Taro"], ["age", 30], ["city", "Tokyo"]]
 
 for (const [key, value] of Object.entries(obj)) {
    console.log(`${key}: ${value}`);
    // name: Taro
    // age: 30
    // city: Tokyo
 }


オブジェクトのプロパティを反復する場合、Object.entries()for...of の組み合わせが最も安全で明示的な方法である。


for系ループの比較

forfor...offor...in の特性を比較した表を以下に示す。

for / for...of / for...in の比較表
特性 for for...of for...in
主な用途 カウンタベースの反復 イテラブルの要素反復 オブジェクトのプロパティキー列挙
主な対象 任意 (カウンタで制御) 配列, 文字列, Map, Set, NodeList等 オブジェクト
各反復で取得できる値 カスタム (インデックス等) 要素の値 プロパティキー (文字列)
導入バージョン ES1 (初期) ES2015 (ES6) ES1 (初期)
break / continue 使用可能 使用可能 使用可能
インデックスの取得 直接アクセス可能 entries() で取得 キー自体がインデックス文字列
配列での使用 推奨 (インデックス操作が必要な場合) 推奨 (値のみの反復) 非推奨
プロトタイプチェーンの影響 なし なし あり (継承プロパティも列挙)
サロゲートペアへの対応 非対応 (分割される) 対応 (1文字として扱う) 非対応


使い分けの指針

3種類のループの使い分けの指針を以下に示す。

  • 配列の値を順番に処理する。
    for...of を使用する。
    シンプルで可読性が高く、インデックスを意識しなくてよい。

  • インデックスが必要な配列の反復
    for 文を使用する。または for...ofarray.entries() を組み合わせる。

  • 文字列をUNICODEコードポイント単位で正しく処理する。
    for...of を使用する。
    絵文字等のサロゲートペア文字を1文字として扱うことができる。

  • Map または Set の反復
    for...of を使用する。
    それぞれのコレクションのイテラブルインターフェースをそのまま活用できる。


  • オブジェクト自身のプロパティを安全に反復する。
    Object.entries()for...of を組み合わせる。
    プロトタイプチェーンの影響を受けない。

  • 継承プロパティを含めてオブジェクトのプロパティを列挙する。
    for...inObject.hasOwn() を組み合わせる。
    自身のプロパティのみを対象にする場合は、Object.hasOwn() でフィルタリングする。

  • 特定の条件が満たされるまで反復する。(回数が不定の場合)
    for (;;)break を組み合わせる。
    または while 文を使用する。


使い分けを示す例を以下に示す。

 // 配列の値のみが必要: for...of
 const numbers = [1, 2, 3, 4, 5];
 for (const num of numbers) {
    console.log(num * 2);
 }
 
 // 配列のインデックスが必要: for
 for (let i = 0; i < numbers.length; i++) {
    console.log(`numbers[${i}] = ${numbers[i]}`);
 }
 
 // オブジェクトのプロパティ反復: Object.entries + for...of
 const config = { host: "localhost", port: 3000, debug: true };
 for (const [key, value] of Object.entries(config)) {
    console.log(`${key}: ${value}`);
 }
 
 // 文字列をコードポイント単位で処理: for...of
 const text = "Hello 😀 World";
 for (const char of text) {
    console.log(char);
 }



関連情報