JavaScriptで配列やオブジェクトをループする良い方法を真剣に検討してみた

  • 90
    いいね
  • 6
    コメント

はじめに

JavaScriptで配列やオブジェクトをループする時どのように書きますか?
シンプルにfor文?
Array.forEachを使う方法もありますね。
あるいはES6から取り入れられたfor-of文でしょうか?
ただこうしたいくつかの方法がある中、僕が以前思ったのは
「で、結局何が良いんだってばよ??( ̄へ ̄|||)」ということです:disappointed_relieved:
単なる文法の違いでどれでも良いのか?それともリスクやパフォーマンスがそれぞれ違ったりするだろうか・・?
その疑問の答えを出すために、ループの各手法を主にリスクパフォーマンスの面で比較・調査して「配列やオブジェクトをループする良い方法」の結論を出してみました。
もし良ければ参考にしてみてください。

どんなループ方法があるか

まず配列とオブジェクトそれぞれでどんなループ方法があるかを挙げます。
※jQueryやUnderscore.jsでのループ方法はここでは取り上げません。またArray.someやArray.everyなどの関数も用途が限定されてますので除外します。
ここで取り上げるのはあくまで汎用的に使用するループ方法です。

配列

for

let ar = [10, 20, 30];

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

for...in

for(let i in ar) {
  console.log(ar[i]);
}

for...of

for(let v of ar) {
  console.log(v);
}

for...ofとArray.entriesの組み合わせ

for...ofを使う場合に添字と値をいっぺんに取り出したい場合

for(let [i, v] of ar.entries) {
  console.log(v);
}

Array.forEach

ar.forEach(function(v, i, a){
  console.log(v);
});

オブジェクト

for...in

let obj = {test1: 10, test2:20, test3:30};
for(let k in obj) {
  console.log(obj[k]);
}

for...ofとObject.keysの組み合わせ

for(let k of Object.keys(obj)) {
    console.log(obj[k]);
}

Array.forEachとObject.keysの組み合わせ

Object.keys(obj).forEach(function (k) {
    console.log(obj[k]);
});

ループ方法の比較

ではこれまでに挙げたループ方法を実際に比較してみます

リスク

ここで言うリスクとは

  • 予期せぬプロパティが参照されてしまわないか
  • prototypeが拡張された場合にさかのぼって参照されてしまわないか

を指します。

試しに配列にプロパティを追加・prototype拡張を行った後ループを行ったらどうなるかを検証してみました。


let ar = [10, 20, 30];
// prototype拡張
Array.prototype.extendTest = "prototype拡張";
// Arrayオブジェクトにプロパティ追加
ar.test = "test";

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

console.log("-----------------");
console.log("for...in");
for(let i in ar) {
    console.log(ar[i]);
}

console.log("-----------------");
console.log("for...of");
for(let v of ar) {
    console.log(v);
}

console.log("-----------------");
console.log("for-of entries");
for(let [k, v] of ar.entries()) {
    console.log(v);
}

console.log("-----------------");
console.log("Array.forEach");
ar.forEach(function(v){
  console.log(v);
});

結果は以下の通り。

for
10
20
30
-----------------
for...in
10
20
30
test
prototype拡張
-----------------
for...of
10
20
30
-----------------
for...of entries
10
20
30
-----------------
Array.forEach
10
20
30

この中でfor...inだけ出力が違うことが分かります。
理由としてfor...inはオブジェクトの"プロパティ"に対してループしているからです. またprototypeをさかのぼって列挙するので拡張されていたら, それもループの対象となってしまいます。

配列ではなくオブジェクトの場合も同様です。
Objectオブジェクトのprototypeが拡張されていれればループの対象になります。

let obj = {test1:10, test2:20, test3:30};
// prototype拡張
Object.prototype.extendFunc = function(){};

console.log("for...in")
for(let k in obj) {
    console.log(obj[k]);
}

console.log("-----------------");
console.log("for...of");
for(let k of Object.keys(obj)) {
    console.log(obj[k]);
}

console.log("-----------------");
console.log("Array.forEach");
Object.keys(obj).forEach(function (k) {
    console.log(obj[k]);
});
for...in
10
20
30
[Function]
-----------------
for...of
10
20
30
-----------------
Array.forEach
10
20
30

従って配列のループにはfor...inを使わない方が良いのと、
オブジェクトのループとして使う場合でも、ループ内でhasOwnPropertyメソッドでそのオブジェクトが持っているプロパティかどうかをチェックする必要があります。

for(let k in obj) {
  if(obj.hasOwnProperty(k)) {
    console.log(k+ ':' + obj[k]);
  }
}

パフォーマンス

各ループ方法で10000回ループした時と100回ループした時の実行速度を比較します。
[テスト条件]

  • 実行環境:Chrome最新版
  • 純粋なループ処理の速度を求める為、ループ内では何も処理しない。
  • 10回実行して平均実行時間を算出
  • 公平を期すため値の評価まで行う

配列

まずは10000回ループを実行!

// テスト用配列作成
let ar = [];
for(let i = 0; i < 100; i++) {
    ar[i] = "test";
}

console.time('for');
for (let i = 0, l = ar.length; i < l; i++) {
  ar[i];
}
console.timeEnd('for');

console.time('for...in');
for(let i in ar) {
  ar[i];
}
console.timeEnd('for...in');

console.time('for...of');
for(let v of ar) {
  v;
}
console.timeEnd('for...of');

console.time('for...of and Array.entries');
for(let pair of ar.entries()) {
  pair[1];
}
console.timeEnd('for...of and Array.entries');

console.time('Array.forEach');
ar.forEach(function(v){
  v;
});
console.timeEnd('Array.forEach');
for for...in for...of for...of and Array.entries Array.forEach
0.208ms 2.242ms 1.305ms 2.184ms 0.522ms

for...inやfor...of and Array.entriesが一番遅いですね。。
逆に一番速いのがfor文ですね。

ただ10000回ループする処理というのは実際のアプリケーションでは現実的ではありませんので、今度は100回にして同じように実行します。

for for...in for...of for...of and Array.entries Array.forEach
0.031ms 0.061ms 0.074ms 0.117ms 0.102ms

100回だとArray.forEachやfor...of and Array.entriesが遅いです。
ただ実行環境にもよるので一概にこれが確実に遅いということは言えません。
やはり速いのがfor文です。
とはいっても0.0*msの世界なので膨大なループ数でなければどのループ方法を使用してもそこまで速度に大差が無いということが分かりました。

オブジェクト

配列と同じように比較します。

// テスト用オブジェクトの作成
let obj = {};
for (let i = 0; i < 100; i++) {
    obj[`test${i}`] = "test";
}

console.time('for...in');
for (let k in obj) {
  obj[k];
}
console.timeEnd('for...in');

console.time('for...of and Object.keys');
for(let k of Object.keys(obj)) {
  obj[k];
}
console.timeEnd('for...of and Object.keys');

console.time('Array.forEach and Object.keys');
Object.keys(obj).forEach(function (k) {
  obj[k];
});
console.timeEnd('Array.forEach and Object.keys');

[ループ10000回実行速度]

for...in for...of and Object.keys Array.forEach and Object.keys
5.58ms 7.803ms 6.074ms

[ループ100回実行速度]

for...in for...of and Object.keys Array.forEach and Object.keys
0.053ms 0.149ms 0.13ms

for...in、Array.forEachとObject.keysの組み合わせ、for...ofとObject.keysの組み合わせの順に速い結果となりました。
特にfor...inが他に比べて一段速いですね。

その他

今まで挙げた方法の中で、ループの途中で走査を止めたい場合、for・for...in・for...ofはbreak文を使えば出来ます。
しかし、Array.forEachは止められない:expressionless:
break文は使用できないので、もし本当に途中で止めたいのであればtry-catch文でやる方法もありますが、、おすすめはしません(汗)

結論

配列のループ

シンプルにfor文を使いましょう:sweat_smile:
もしくはfor...ofでも良いです。
逆にfor...in文はリスクの面で使わないようにするのが賢明です。

オブジェクトのループ

for...ofとObject.keysの組み合わせがベターかと。
for...inは~(以下同文(苦笑))
もしループの途中で止めることが無ければArray.forEachとObject.keysの組み合わせでも良いと思います。

もし他に良い方法のループがあれば是非ご意見を下さい。