LINDORのAdvent Calendar(本物)の15日目を開けたところ。
配列とかおれおれAdvent Calendar2018 – 15日目

繰り返しにもいろいろあるよ。

for-in じゃない方です。 こっち使おう。

for-of

for-in と違って値の方を持ってきてくれます。

const arr = [11, 22, 33];

for (const value of arr) {
  console.log(value);
}

// 11
// 22
// 33

for に置き換え

反復可能なオブジェクトを対象とする for-of を普通の for 文に置き換えることもできます。

const arr = [11, 22, 33];

const it = arr.values();
for (let cur = it.next(); !cur.done; cur = it.next()) {
  const { value } = cur;
  console.log('for', value);
}

仕様

for-in, for-of, for-await-of は全部まとめて定義されてる。

ので、 for-in の方もみといてください。

for-in は列挙可能なプロパティ名を得ましたが、 for-of の方では反復子 (iterator) を用いた繰り返しを行います。

利用可能なオブジェクトと反復子 (iterator)

for-of は「反復可能」なオブジェクトでのみ利用可能です。

この「反復可能 iterable」であるとは、まあ本稿は for-of が主眼ですのでざっくり申し上げると、 [Symbol.iterator] メソッドがその 反復子 iterator オブジェクトを生成するよう適切に用意されている状態を言います。

自作もできます。

const obj = {
  * [Symbol.iterator] () {
    yield 11;
    yield 22;
    yield 33;
  },
};

詳しくは別稿(予定)をご覧ください。

で、配列は反復可能なオブジェクトなので使えます。

console.log(Symbol.iterator in arr); // => true

他に SetMap それから String オブジェクトも反復可能です。 (文字列 "" 自体はだめ。オブジェクトでないのでメソッドもない。呼び出せるけど。)

未対応のオブジェクトを繰り返す

配列で使う分には簡単だったけど、普通のオブジェクトはそのままでは使えません。ひと手間必要。

const obj = {};
for (const value of obj) {
}
// TypeError: obj is not iterable

次の三通りのメソッドで、何の変哲もないただのオブジェクトから配列を生成します。 前項の通り、配列なら反復可能。

  • Object.values()
  • Object.keys()
  • Object.entries()

反復可能でない通常のオブジェクトはこれら通して反復可能なオブジェクトを得、それを for-of へ与えることができる、という算段です。

以下、こういうオブジェクトがある前提でコード例を提示します。

const obj = {
  foo: 11,
  bar: 22,
  boo: 33,
};

プロパティ値を繰り返す

for (const value of Object.values(obj)) {
  console.log(value);
}

// 11
// 22
// 33

プロパティ名を繰り返す

for (const key of Object.keys(obj)) {
  console.log(key);
}

// foo
// bar
// boo

名前と値を繰り返す

両方か!? keyvalue の両方ほしいのか? 両方……イヤしんぼめ!!

はい、そんな欲張りさんのための機能もあります。ちょっとわかりづらいんだけど、戻り値としてその2つだけを格納した配列の配列を返します。

for (const [key, value] of Object.entries(obj)) {
  console.log(key, ':', value);
}

// foo : 11
// bar : 22
// boo : 33

得られる順序

3つとも内部処理は同じこれ↓で、得られる順序は以下の通り。

  1. 整数インデックス昇順(を文字列にしたもの)
  2. 文字列キー追加順
const obj = {};

obj[2] = '#1';
obj.z = '#2';
obj[Symbol(1)] = '#3';
obj[10] = '#4';
obj.a = '#5';

console.log(obj);
// => { '2': '#1', '10': '#4', z: '#2', a: '#5', [Symbol(1)]: '#3' }

for (const [key, value] of Object.entries(obj)) {
  console.log(key, ':', value);
}

// 2 : #1
// 10 : #4
// z : #2
// a : #5

仕様はこちら。

ちなみに OrdinaryOwnPropertyKeys() ではキーが Symbol のものも追加順に取得しているのだけれど、その後 EnumerableOwnPropertyNames() の処理で文字列でないものは捨てられます。

先の例↑で console.log() の方では Symbol がキーになってるものも取れているのは、その処理が OrdinaryOwnPropertyKeys() を使ってるからなんでしょう。知らんけど、コンソールの仕様は。

任意の順序に

したい場合、別途 sort() してやります。

文字列順とか localeCompare() が便利っぽい。 (ちなみに local ではなく locale です。)

// プロパティ名昇順
const it = Object.entries(obj)
  .sort(([key1], [key2]) => key1.localeCompare(key2));
for (const [key, value] of it) {
  console.log(key, ':', value);
}

// bar : 22
// boo : 33
// foo : 11

sort() に与えた比較関数は実は省略可能なんですが、罠があったりするので、常に省略しないのが良いやり方かと思っております。

おしまい

強力。

関連

参考