LoginSignup
123
113

More than 5 years have passed since last update.

JavaScriptにおける繰り返し処理の使い分け

Posted at

while 文

繰り返し処理を行う「最も基本となる文」です。
原始的故にコードの記述量は多くなりますが、処理速度は基本的に最速です。

/**
 * 1~10の繰り返し処理
 */
var i = 0;
while (i < 10) {
  console.log(i++);
}

/**
 * 配列の繰り返し処理
 */
var array = ['a', 'b', 'c', 'd', 'e', 'f'], i = 0, len = array.length;
while (i < len) {
  console.log(array[i++]);
}

/**
 * イテレータの繰り返し処理
 */
var mapIterator = new Map([[0, 'a'], [1, 'b'], [2, 'c'], [3, 'd'], [4, 'e']]).entries(), iteratorResult;
while (iteratorResult = mapIterator.next(), !iteratorResult.done) {
  console.log(iteratorResult.value);
}

for 文

while 文に「事前処理文」「繰り返し処理文」を加えた文です。
事前に変数宣言を出来て繰り返し処理も for 文内で可能なので、配列の繰り返し処理に向いています。
(逆にイテレータの繰り返し処理には不向きです。下記コードでは Map.prototype.size を利用する事で for 文で繰り返し処理を実現していますが、基本的にイテレータには長さを得る方法がない為、原始的な繰り返し処理としては while 文が向いています。)

/**
 * 1~10の繰り返し処理
 */
for (var i = 0; i < 10; ++i) {
  console.log(i);
}

/**
 * 配列の繰り返し処理
 */
var array = ['a', 'b', 'c', 'd', 'e', 'f'];
for (var i = 0, len = array.length; i < len; ++i) {
  console.log(array[i]);
}

/**
 * イテレータの繰り返し処理
 */
var map = new Map([[0, 'a'], [1, 'b'], [2, 'c'], [3, 'd'], [4, 'e']]), mapIterator = map.entries();
for (var i = 0, size = map.size; i < size; ++i) {
  console.log(mapIterator.next().value);
}

for-of 文

for-of 文は「イテレータの繰り返し処理」に使われます。
ES6(ES2015)から配列もイテレータの一部と再定義された為、配列にも適用可能です。
しかしながら、for-of 文は IE11- で使用できない為、「IE をサポートしない」「Babel でトランスコンパイルする」等の打開策が必要になります。

/**
 * 配列の繰り返し処理
 */
var array = ['a', 'b', 'c', 'd', 'e', 'f'];
for (var value of array) {
  console.log(value);
}

/**
 * イテレータの繰り返し処理
 */
var mapIterator = new Map([[0, 'a'], [1, 'b'], [2, 'c'], [3, 'd'], [4, 'e']]).entries();
for (var entry of mapIterator) {
  console.log(entry);
}

Array.prototype.forEach

Array.prototype.forEach は ES5 で追加された配列用の繰り返し処理メソッドです。
IE8- では実装されていませんが、Microsoftは2016/01/12にIE8のサポートを終了したので、特殊な案件でなければまず使えます(IE8- をサポートする事情があるとしても Polyfill で対応可能)。
今までの繰り返し処理文と違い、forEach には配列の中で存在する要素だけを取り出すという重要な性質があります。
また、基本的に関数コストは高い為、他の文による処理と比較すると forEach は遅いです。

/**
 * 配列の宣言
 */
var array = [1,,3,4,5]; // index 1 が存在しない配列を宣言する

/**
 * 配列の繰り返し処理 (Array.prototype.forEach)
 */
array.forEach(function (value, index, array) {
  console.log(value); // 1 -> 3 -> 4 -> 5
});

/**
 * 配列の繰り返し処理 (for-of 文)
 */
for (var value of array) {
  console.log(value); // 1 -> undefined -> 3 -> 4 -> 5
}

/**
 * 配列の繰り返し処理 (for 文)
 */
for (var i = 0, len = array.length; i < len; ++i) {
  console.log(array[i]); // 1 -> undefined -> 3 -> 4 -> 5
}

/**
 * 配列の繰り返し処理 (while 文)
 */
var i = 0, len = array.length;
while (i < len) {
  console.log(array[i++]); // 1 -> undefined -> 3 -> 4 -> 5
}

/**
 * 配列の繰り返し処理 (for 文 + Object.prototype.hasOwnProperty)
 */
for (var i = 0, len = array.length; i < len; ++i) {
  if (array.hasOwnProperty(i)) {
    console.log(array[i]); // 1 -> 3 -> 4 -> 5
  }
}

/**
 * 配列の繰り返し処理 (while 文 + Object.prototype.hasOwnProperty)
 */
var i = -1, len = array.length;
while (++i < len) {
  if (array.hasOwnProperty(i)) {
    console.log(array[i]); // 1 -> 3 -> 4 -> 5
  }
}

for-in 文

オブジェクトにおける列挙可能(enumerable: true)な「直属のプロパティ名」「プロトタイプ上のプロパティ名」を繰り返し取得します。
このプロトタイプ上のプロパティを取得する挙動が問題になるケースが多々あります。

Object.prototype.c = 3;
var object = {a: 1, b: 2};

for (var key in object) {
  console.log(key); // "a" -> "b" -> "c"
}

上記コードで "c" は取得する必要がなかったプロパティであり、この問題に対処する方法は大きく分けて2つあります。

  • Object.keys で直属のプロパティだけを列挙する(プロトタイプ上のプロパティは列挙しない)
  • Object.definePropertyObject.prototype.c を列挙不可能なプロパティとして定義する

解決手段となるコードは次の節にあるのでそちらを参考にしてください。

※この事象からいえるのは「for-in 文がプロトタイプ上のプロパティを列挙したい時に使うべき」という事です。
「プロトタイプ上のプロパティ」の考え方が理解できずにはまる人が多いので、プロトタイプチェーン(prototype-chain)の理解が進むまでは for-in 文には手を出さない方がいいかもしれません。

オブジェクトのkey/valueを列挙する

オブジェクト初期化子を連想配列であるかのように扱い、key/valueを列挙するコードは随所に見られますが、列挙する方法にもいろいろあります。
しかし、オブジェクト初期化子を連想配列として扱うにはいくつかの障害があります。

  • 列挙順は実装依存であり、規則性がない
  • 列挙順は全ての方法で同一でなければならない
  • {} で初期化したオブジェクトは __proto__ 等の既定値が存在し、プロパティアクセス演算子では上書きできない(例えば、__proto__ に 1 を代入できない)

これらの問題を解決する方法として Object.create(null)new Map がありますが、説明すると長くなるので機会があれば別記事で説明したいと思います。

/**
 * オブジェクトの宣言
 */
var object = {a: 1, b: 2};

/**
 * オブジェクトのプロパティを定義する
 */
Object.defineProperty(object, 'c', {  // 列挙不可能(enumerable: false)なプロパティ "c" を定義する
  configurable: true,
  enumerable: false,  // 列挙不可能
  writable: true,
  value: 3
});

Object.prototype.d = 4; // プロトタイプ上に列挙可能なプロパティ "d" を定義する

Object.defineProperty(Object.prototype, 'e', {  // プロトタイプ上に列挙不可能なプロパティ "e" を定義する
  configurable: true,
  enumerable: false,  // 列挙不可能
  writable: true,
  value: 5
});

/**
 * for-in 文
 * オブジェクトにおける列挙可能(enumerable: true)な「直属のプロパティ名」「プロトタイプ上のプロパティ名」を繰り返し取得する
 */
for (var key in object) {
  console.log(key); // "a" -> "b" -> "d"
}

for (var key in Object.prototype) {
  console.log(key); // "d"
}

/**
 * Object.getOwnPropertyNames (ES5)
 * オブジェクトにおける列挙可能/列挙不可能な「直属のプロパティ名」を列挙する
 */
console.log(Object.getOwnPropertyNames(object));           // ["a", "b", "c"]
console.log(Object.getOwnPropertyNames(Object.prototype)); // ["__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", "__lookupSetter__", "propertyIsEnumerable", "constructor", "toString", "toLocaleString", "valueOf", "isPrototypeOf", "__proto__", "d", "e"]

/**
 * Object.keys (ES5)
 * オブジェクトにおける列挙可能(enumerable: true)な「直属のプロパティ名」を列挙する
 */
console.log(Object.keys(object));           // ["a", "b"]
console.log(Object.keys(Object.prototype)); // ["d"]

/**
 * Object.values (ES8 / ES2017)
 * オブジェクトにおける列挙可能(enumerable: true)な「直属のプロパティ値」を列挙する
 */
console.log(Object.values(object));           // [1, 2]
console.log(Object.values(Object.prototype)); // [4]

/**
 * Object.entries (ES8 / ES2017)
 * オブジェクトにおける列挙可能(enumerable: true)な「直属のプロパティ名」「直属のプロパティ値」を列挙し、[key, value] から構成される二次元配列を取得する
 */
console.log(JSON.stringify(Object.entries(object)));           // [["a",1],["b",2]]
console.log(JSON.stringify(Object.entries(Object.prototype))); // [["d",4]]

Map.prototype.forEach

ES6(ES2015)で追加された new Map は「オブジェクト初期化子の連想配列版」とも呼べる仕組みでJavaScriptで連想配列を使いたかった人の要望の多くを満たしています。

  • プロトタイプ上のプロパティと干渉しない
  • Map.prototype.forEach によってkey/valueを定義順で列挙できる
  • key に String 型しか指定できない制限はなく、全ての型を指定できる
  • key に NaN を指定した場合も Map.prototype 系メソッドで同値として判定できる
  • Map.prototype.entries, Map.prototype.keys, Map.prototype.values でイテレータを出力できる
var map = new Map([[0, 'a'], [1, 'b'], [2, 'c'], [3, 'd'], [4, 'e']]);

map.forEach(function (value, key, map) {
  console.log([key, value]);  // [0, "a"] -> [1, "b"] -> [2, "c"] -> [3, "d"] -> [4, "e"]
});

まとめ

概ね、次の使い分けになると思います。

  • イテレータには for-of 文を使う
  • 配列には for 文、for-of 文、Array.prototype.forEach を使い分ける
  • その他、ビルトイン関数があればそれを使う
  • パフォーマンス重視なら while 文を使う
  • 連想配列には new Map もしくは Object.create(null) を使う
123
113
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
123
113