for…of の「見た目」と「中身」は別物だと思ってみる
for...of は、表面上はとてもシンプルです。
const arr = [10, 20, 30];
for (const value of arr) {
console.log(value);
}
// 10
// 20
// 30
JavaScript「配列の中身を順番に取り出してくれる便利な構文」に見えますが、
実は裏側でかなりキッチリした仕組みが動いています。
その仕組みのキーワードが次のふたつです。
Iterable(イテラブル)Iterator(イテレータ)
これらを理解すると、for...of は
「魔法の構文」ではなく「イテレータを自動で回してくれる糖衣構文」に見えてきます。
ここが重要です。
for…of は「配列専用の文法」ではなく、「イテラブルなものなら何でも回せる“汎用のループ構文”」です。
この「イテラブルを回す」という目線が持てるかどうかが、理解の分かれ目です。
for…of が内部でやっている基本ステップ
まずは [Symbol.iterator] を呼び出す
for…of が最初にやることは、「対象がイテラブルかどうか」の確認です。
配列、文字列、Map、Set などは、すべて[Symbol.iterator] という特別なメソッドを持っています。
const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator](); // これは手動でやっているだけ
console.log(typeof iterator.next); // "function"
JavaScript[Symbol.iterator]() を呼び出した結果が、「イテレータ」です。
イテレータは必ず next() メソッドを持っています。
次に next() を繰り返し呼び出す
イテレータの next() を呼ぶと、
必ず { value: 何か, done: true/false } というオブジェクトが返ってきます。
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
JavaScriptdone: false の間は、まだ値が残っています。done: true が返ってきたら「もう終わり」です。
for…of はこの処理を自動で書いてくれている
つまり、次の二つは同じことをしています。
for…of を使ったコード:
for (const value of arr) {
console.log(value);
}
JavaScript内部的にやっていそうな処理を手で書くとこうなります:
const iterator = arr[Symbol.iterator]();
while (true) {
const result = iterator.next();
if (result.done) {
break;
}
const value = result.value;
console.log(value);
}
JavaScriptこれが for…of の正体です。
「イテレータを取得 → next() を何度も呼ぶ → done: true で終了」
この一連の流れを、自動でやってくれる構文なのです。
ここが重要です。
for…of を見るとき、「内部で iterator.next() が回っている」とイメージできるかどうか。
これが分かると、Iterator / Generator とのつながりが一気に見えてきます。
for…of が使える条件:「イテラブル」であること
イテラブルとは「[Symbol.iterator] を持っているもの」
配列や文字列が for…of で回せるのは、
「たまたま配列だから」ではなく、「イテラブルだから」です。
イテラブルの条件はとてもシンプルで、obj[Symbol.iterator] が「イテレータを返す関数」であることです。
配列で試してみます。
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // "function"
const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 1, done: false }
JavaScript文字列も同じです。
const str = "Hi";
console.log(typeof str[Symbol.iterator]); // "function"
const it2 = str[Symbol.iterator]();
console.log(it2.next()); // { value: "H", done: false }
JavaScript自作オブジェクトを for…of 対応にしてみる
例えば、「1 から n まで数えるオブジェクト」を自分で作ってみましょう。
const counter = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
},
};
for (const num of counter) {
console.log(num);
}
// 1 2 3 4 5
JavaScriptこのオブジェクトは配列でも Map でもありません。
でも [Symbol.iterator] を実装したことで、
for…of が「イテラブル」と認識してくれます。
ここが重要です。
for…of が回せるかどうかは、「配列かどうか」ではなく
「イテレータを返す [Symbol.iterator] を持っているかどうか」で決まる。
この視点を持てると、for…of が一気に“言語のルール”として理解できます。
break / continue / return とイテレータの関係
break したときのイメージ
for…of を途中で break すると、
「裏側の while ループを途中で抜ける」と考えられます。
const arr = [1, 2, 3, 4, 5];
for (const n of arr) {
if (n > 3) break;
console.log(n);
}
// 1 2 3
JavaScript内部イメージにすると、
const it = arr[Symbol.iterator]();
while (true) {
const { value, done } = it.next();
if (done) break;
if (value > 3) break;
console.log(value);
}
JavaScriptという感じです。
基本的な配列や文字列のイテレータでは、
特別な後処理はありませんが、
自作のイテレータや一部のライブラリでは、
「ループが最後まで行かなかったときに cleanup をしたい」ということもあります。
return(関数ごと抜ける)も同じように途中終了
for…of の中で return すると、
当然ループも途中で終了します。
function findFirstEven(arr) {
for (const n of arr) {
if (n % 2 === 0) {
return n;
}
}
return null;
}
console.log(findFirstEven([1, 3, 4, 6])); // 4
JavaScript内部イメージ的には、「next() を途中までしか呼ばないでループを抜けた」という状態になります。
Generator と for…of の組み合わせ
Generator(function*)は、それ自体が
「イテレータでもあり、イテラブルでもある」オブジェクトを返します。
function* gen() {
yield 1;
yield 2;
yield 3;
}
for (const v of gen()) {
console.log(v);
}
// 1 2 3
JavaScriptfor…of はここでも、gen() が返すオブジェクトから [Symbol.iterator] を呼び出し、next() を繰り返しているだけです。
Generator の章でやったことが、そのまま for…of に直結しているのが分かると思います。
for…in と for…of の違い(内部的な意味の違い)
for…in は「キー(プロパティ名)用」
const obj = { a: 1, b: 2 };
for (const key in obj) {
console.log(key); // "a", "b"
console.log(obj[key]); // 1, 2
}
JavaScriptfor…in は「プロパティ名(キー)」を列挙するための構文です。
対象はオブジェクトで、
内部的には in 演算子とプロトタイプチェーンのルールに従ってキーを列挙します。
配列に使うと、「インデックス(”0″, “1”, … の文字列)」が取れてくるので、
通常は配列には向きません。
for…of は「値用」、イテラブルが対象
const arr = [10, 20, 30];
for (const value of arr) {
console.log(value); // 10, 20, 30
}
JavaScriptfor…of は、「イテラブル([Symbol.iterator] を持つもの)」から
「値」を順番に取り出すための構文です。
内部では iterator.next() を呼び続けているだけなので、
オブジェクト {} のようにイテレータを持たないものには使えません。
ここが重要です。
for…in は「キー列挙」、for…of は「イテレータを回して値列挙」。
まったく別の仕組みで動いている構文だということを、混同しないようにしてください。
まとめ:for…of の内部モデルを一文で言うと
for…of は、内部的には
対象Symbol.iterator でイテレータを取得し、
その iterator.next() を繰り返し呼びながら、
done: true になるまで value を順番に取り出す構文
です。
押さえておきたいポイントを整理すると、次のようになります。
for…of が回せるのは、「[Symbol.iterator] を持つイテラブル」である
配列・文字列・Map・Set・Generator などは、みんなこのルールに従っている
for…of は、裏で iterator.next() を呼んで { value, done } を見ているだけ
自作オブジェクトに [Symbol.iterator] を実装すれば、それも for…of 対応にできる
for…in は「キー列挙」、for…of は「イテレータによる値列挙」と役割が違う
最後に、練習としてこうしてみてください。
まず普通に for…of を書き、そのあとに
「この for…of は内部的にはこうなってるはず」と思って[Symbol.iterator]() と next() を自分の手で呼び出すコードを書いてみる。
それを一度でもやると、
for…of の「中にいるイテレータの姿」が、かなりはっきり見えてくるはずです。
