iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

【JavaScript】for文を使い分ける

こんにちは。

iimonでエンジニアをしているかねにわです。

JavaScriptにおけるループ処理には、基本的なfor文からfor…of文など、いくつか種類がありますが、正直のところそれぞれの違いや特性をあまり理解せず、とりあえずでforEachを使っているような状態でした。

本記事では、JavaScriptの代表的なループ処理であるfor文、for…in文、for…of文、forEachについて、それぞれの特徴および使い分けを整理しました。

はじめに書いてしまうと、上記4つのループ文の違いは、ざっと以下のようになります。

これを踏まえ、それぞれの特徴を見ていきます。

目次

for文

みなさんお馴染みの、もっともベーシックなfor文です。 以下のような構文で、カウンタ変数を指定の条件を満たすまでループを行います。

for(初期化処理;  継続条件; ループの最後に実行する式){
    継続条件がtruthyの場合に実行させる処理
}

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

ループの開始や更新、終了条件を自由に設定できるのに加え、breakやcontinueとも組み合わせて柔軟な処理が可能です。

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
for (let i = 0; i < array.length; i++) {
  if (array[i] === 7) {
    break;
  }
  if (array[i] % 2 === 0) {
    continue;
  }
  console.log(array[i]);
}

for...in文

こちらは主にオブジェクトに対して使われ、後述する列挙可能(enumerable)なプロパティを取り出します。また、こちらも後述しますが配列に対する使用は推奨されません。

構文は以下の通りで、オブジェクトの値ではなくキーが抽出されます。配列の場合はインデックスが文字列の形で抽出されます。

const obj = {
  name: '大和',
  age: 45,
};

for (const key in obj) {
  console.log(key);
}
// name
// age

ここで特に注意が必要なのが、for…in文ではプロトタイプ拡張されたプロパティまで一緒にループ対象として抽出されてしまう点です。

プロトタイプとは?

基本的にほぼ全てのオブジェクト(配列や連想配列、関数等)はObjectを継承しています。また、このObjectはprototypeというオブジェクトを持っており、全てのオブジェクトは暗黙的にこのprototypeも継承しています。そして、prototypeに定義されたプロパティを継承していることになります。

例えば、以下のようにオブジェクトリテラルを使って、nameプロパティのみを持つごくシンプルなオブジェクトを定義し、toStringメソッドを呼び出してみます。

const obj = {
 name: '大和'
}

console.log(obj.toString())
// [object Object]

ここで少し考えてみると、生成したobjはnameプロパティしか定義していないのに、toStringメソッドが呼び出せるのは不思議です(toStringはどこから来た?)。

これは、toStringはあらゆるオブジェクトの継承元であるObject.prototypeオブジェクトにて定義されているメソッドであり、したがって今回生成したobjもこのprototypeを暗黙のうちに継承しているために、toStringメソッドが呼び出せると言うわけです。

これを踏まえて、prototypeに新しくプロパティを追加してみます。

Object.prototype.hoge = () => {}

この状態で、オブジェクトを生成しfor…in文を適用すると、

const obj = {
 name: '大和'
}

for (const item in obj) {
  console.log(item);
}
// name
// hoge ←拡張されたプロパティまで取得されてしまう

nameプロパティだけを持つobjに対しループしているのでnameプロパティしか出力されないはずが、先ほどプロトタイプに追加したhogeプロパティまで取得されてしまっています。これが、for…in文ではプロトタイプ拡張されたプロパティまで一緒に取得されてしまうということです。

ここで少し立ち止まって考えてみます。今回意図的にプロトタイプに追加したhogeが取得されるのはいいとして、先述したtoStringメソッドもプロトタイプに属するオブジェクトのため、これも取得されるのが道理のはずですが、そうなっていません。これはどういうことでしょうか?

これは、for…in文の列挙可能(enumerable)なプロパティのみを取り出すという性質によるものです。

簡単に言ってしまうと、enumerable属性がtrueなプロパティのみがfor…in文の取得対象となります。

この属性は、以下のようにObject.getOwnPropertyDescriptorメソッドを使うことによって確認できます。

console.log(Object.getOwnPropertyDescriptor(Object.prototype, 'toString'))

{
  "writable": true,
  "enumerable": false, ←falseになっているので列挙対象外。
  "configurable": true
}
console.log(Object.getOwnPropertyDescriptor(Object.prototype, 'hoge'))

{
  "value": "hoge",
  "writable": true,
  "enumerable": true, ←trueなので列挙可能としてループ対象となってしまう。
  "configurable": true
}

for…in文を使う際、このようにプロトタイプ拡張されたプロパティを弾くには、以下のようにhasOwnPropertyを使うことで、継承されていない自身のオブジェクト固有のプロパティのみを取得対象とすることができます。

Object.prototype.hoge = () => {};

const obj = {
  name: '大和',
};

for (const item in obj) {
  if (obj.hasOwnProperty(item)) {
    console.log(item);
  }
}
// name

また、for…inで処理する対象が配列の場合、取得される要素は値ではなくインデックスですが、これは文字列として取得される上、取得される要素の順番が保証されない等の理由から配列への仕様は基本的に非推奨とされています(もっとも、現在のECMAScriptの仕様ではインデックスの整数値は昇順で走査されるなど、処理順序で神経質になる必要はあまりなさそうです)

for...of文

for..of文は、配列のようなイテラブルなオブジェクトに対し処理を行います。

for…inが対象のキーを取り出すのに対し、こちらは値を取り出すので、配列に対して、より直感的な操作が可能です。

また、for…inのように意図しないプロパティを列挙してしまう心配もありません。

const array = ['大和', '武蔵', '長門'];

for (const item of array) {
  console.log(item);
}
// '大和'
// '武蔵'
// '長門

要素だけでなくインデックスも取得したい場合は、以下のようにentries()と分割代入を組み合わせることで実現可能です

const array = ['加賀', '赤城', '蒼龍']

for(const[index, value] of array.entries()){
    console.log(`index:${index}, value:${value}`)
}
// 'index:0, value:加賀'
// 'index:1, value:赤城' 
// 'index:2, value:蒼龍'

また、配列でなくオブジェクトに対してfor...of文を適用したい場合は、Object.keys, Object.values, Object.entriesメソッドを使いましょう。

・Object.keysの場合

const obj = {
    name: '瑞鶴'
    age: 44
}

for(const item of Object.keys(obj)){
    console.log(item)
}
// name
// age

Object.keys(obj)でキーが配列として抽出され、キーが一つずつ取得されます。

・Object.valuesの場合

const obj = {
    name: '瑞鶴'
    age: 44
}

for(const item of Object.values(obj)){
    console.log(item)
}
// 瑞鶴
// 44

Object.values(obj)で値が配列として抽出され、値が一つずつ取得されます。

キーと値どちらも処理したい場合は、Object.entriesを使うと良いでしょう。

const obj = {
    name: '瑞鶴'
    age: 44
}

for(const item of Object.entries(obj)){
    console.log(item)
}
// ['name', '瑞鶴']
// ['age', 44]

この場合は、オブジェクトのキーと値のペアが、配列として取得されます。そのため、分割代入を使っても見通しがよくなるでしょう。

forEach

こちらは主に配列に対して用い、各要素に対して指定した関数(コールバック関数)を一度ずつ実行します。

const baseArray = ['紫電', '震電', '橘花']
baseArray.forEach((value, index, arr) => {
  console.log(index, value, arr);
});
// 0,  '紫電',  ['紫電', '震電', '橘花'] 
// 1,  '震電',  ['紫電', '震電', '橘花'] 
// 2,  '橘花',  ['紫電', '震電', '橘花'] 

筆者はとりあえずでforEachを使ってしまう癖があったのですが、forEachにも留意すべき点があります。それは、continueやbreakを使って途中でループを抜けることができないということです。特に、大きなデータを反復処理する中で、条件の合致や目的の要素に対する処理を終えた場合に、以降のループ処理を行う必要がない場合など、ループを抜けることができないために無駄な処理を走らせてしまうので、こうしたケースではforEachではなくfor…of文等の使用を検討するとよいでしょう。

また、for…of文の項で説明したObject.entries等を組み合わせることでオブジェクトに対してもforEachは使えます。

const obj = {
    name: '瑞鶴', 
    age: 44
};

Object.entries(obj).forEach((item) => {
    console.log(item);
});

// ['name', '瑞鶴']
// ['age', 44]

forEachは基本的に、ループをスキップまたは抜ける必要がない場合に利用するのがよいでしょう。

まとめ

for文それぞれに特徴があり、何を選ぶかは悩ましいところでもありますが、基本的にfor...of文を選択するのが無難かなと思います。 オブジェクトの場合もentriesメソッドと組み合わせて使うこともできますし、continueやbreakを使えるのも地味に強みかなと思います。

ここまでお読みいただき、ありがとうございます。

弊社ではエンジニアを募集しております。

この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

下記リンクよりご応募お待ちしております!

iimon採用サイト / Wantedly / Green

参考文献・記事

外村将大『独習JavaScript 新版』, 2023, 翔泳社

https://swallow-incubate.com/archives/blog/20190930/

https://qiita.com/endam/items/808a084859e3a101ab8f

https://jsprimer.net/basic/prototype-object/

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for...in