似ているけど役割が違う2つのループ
JavaScript には見た目がそっくりな2つのループ構文があります。でも中身はまったく別物です。for...of はイテラブルの値を順に取り出すループで、for...in はオブジェクトのキーを取り出すループ。たった1文字違うだけで、挙動はガラッと変わります。
まずは1つのサンプルで違いを一気に見てみましょう。
最初のループは apple、banana、cherry という値を出力します。一方、2つ目は 0、1、2 というキーを文字列として出力します。使い分けを間違えると、「配列の中身が数字ばっかりになってるのはなぜ?」と10分ほど頭を抱えることになります。
for...of:イテラブルな値を取り出す
for...of は、JavaScriptで一番出番の多いループです。JavaScriptが イテラブル (iterable) とみなすもの、つまり配列、文字列、Map、Set、NodeList、ジェネレーターなど、ほぼ何にでも使えます。
インデックスをいちいち回す必要もなく、scores[i] みたいな書き方もいりません。値が欲しいと言えば、値がそのまま返ってくる。文字列もイテラブルなので、for...of を使えば一文字ずつ取り出せます。
ほとんどの Unicode コードポイントでもきちんと動いてくれるので、文字列をインデックス番号で取り出すのと比べて地味ですが確実なメリットがあります。
for...of でインデックスを取得する方法
for...of が唯一ストレートに渡してくれないのが、要素の位置(インデックス)です。どうしても必要なときは、entries() と組み合わせて使いましょう。
names.entries() は [index, value] というペアを返してくれるので、左辺の分割代入で一気に2つの変数に分けられます。インデックスが欲しいからといって for (let i = 0; ...) に戻るより、こちらの方がスッキリ書けることがほとんどです。
for...in:オブジェクトのキーをループする
for...in は、プレーンなオブジェクトを回すために用意された構文です。列挙可能な文字列キーを順番に取り出します。
取れるのは「キー」であって「値」じゃない、という点に注意してください。値が欲しいときは user[key] のようにオブジェクトへ戻って取りに行きます。キーは常に文字列で、見た目が数字でも文字列として扱われます。
さらに for...in はプロトタイプチェーンもたどるので、継承されたプロパティまで拾ってしまうことがあります。自分で書いたオブジェクトリテラルなら気にならない場面がほとんどですが、クラスのインスタンスやライブラリ由来のオブジェクトをループするときは、念のため対策を入れておくと安心です。
Object.hasOwn(user, key) を使えば、継承されたプロパティは無視できます。ただ最近のコードでは、そもそも for...in を避けて、Object.keys・Object.values・Object.entries を使うのが主流です。というわけで、次のセクションにつながります。
オブジェクトをループする今どきのやり方
for...in の代わりに、for...of と Object.* 系メソッドを組み合わせるのが定番です。以下の3つから、用途に合うものを選びましょう。
Object.entries は特に便利で、[key, value] を分割代入で受け取ると、コードがそのまま読み下せる感覚になります。しかもこれらのメソッドはオブジェクト自身の列挙可能なプロパティだけを返してくれるので、プロトタイプチェーンから継承された余計なものに悩まされる心配もありません。
配列に for...in を使ってはいけない理由
文法としては通ってしまうのですが、思わぬ落とし穴があります。
0、1、2、さらに tag まで出てきます。誰かが配列に付け足したプロパティや、polyfill によって Array.prototype に生えたプロパティも全部引っかかります。しかもキーは文字列なので、key + 1 は足し算ではなく文字列連結になってしまいます。おまけに、エッジケースによっては反復順が配列の順番と一致する保証もありません。
使い分けの目安はこんな感じです:
- 配列の値がほしい →
for...of arr - 配列のインデックスと値の両方がほしい →
for...of arr.entries() - インデックスだけカウントしたい → 昔ながらの
for (let i = 0; i < arr.length; i++) - オブジェクトのキーやエントリを回したい →
Object.keys(obj)やObject.entries(obj)とfor...ofの組み合わせ
要するに、for...in はかなり限定用途のツール。普段は for...of と Object 系のヘルパーがあれば困りません。
break と continue はどちらのループでも使える
どちらのループでも、いつもの早期脱出系の構文がそのまま使えます:
continue で次のループへスキップし、break でループそのものから抜けられます。これこそが for...of を .forEach() より推したい一番の理由です。forEach のコールバックからは break で抜けられませんが、for...of なら普通に使えます。
for of と for in の違いをざっくり比較
4種類のループ、4つの役割、でも考え方はひとつ。イテラブルから値を取り出したいなら for...of。オブジェクトのプロパティを扱いたいなら、Object.keys / Object.values / Object.entries を経由して、これも for...of で回す。for...in は、継承されたものも含めてオブジェクトの列挙可能な文字列キーをすべて本当に舐めたい、そんな珍しいケース専用だと思っておけばOKです。
次は真偽値(Truthy と Falsy)について
JavaScript のループや if は、結局のところ毎回同じ質問をしています。この値は true 扱いなのか? ということ。ところがこの答えが一筋縄ではいかなくて、空文字列や 0、null、undefined はどれも想像と違う動きをすることがあります。次回は、この Truthy(真と見なされる値)と Falsy(偽と見なされる値)について掘り下げていきます。
よくある質問
for...of と for...in の違いは何ですか?
ざっくり言うと、for...of は配列・文字列・Map・Set といったイテラブルな値そのものを取り出すループです。一方 for...in はオブジェクトの**キー(プロパティ名)**を取り出します。例えば ['a', 'b'] に対して for...of なら 'a'、'b' が得られますが、for...in では文字列の '0'、'1' が返ってきます。
for...of でオブジェクトを回せますか?
そのままでは回せません。プレーンなオブジェクトはイテラブルではないからです。Object.keys(obj)、Object.values(obj)、Object.entries(obj) のいずれかで配列に変換してから for...of に渡します。実務でよく見るのは for (const [key, value] of Object.entries(obj)) の形ですね。
配列に for...in を使ってはいけない理由は?
一応動きはしますが、for...in は列挙可能なプロパティをすべてたどってしまいます。継承されたものや、誰かが Array.prototype に追加した余計なプロパティまで拾ってしまう、ということです。しかもキーは文字列として返り、数値インデックスの順序も状況によっては保証されません。値がほしければ for...of、インデックスが必要なら従来の for ループを使うのが無難です。