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

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

for-in は現代ではたぶんもう使う機会ないんじゃないかなと思う。

for-in

列挙可能 (enumerable) なオブジェクトプロパティを繰り返します。

const arr = [11, 22, 33];
for (const index in arr) {
  const value = arr[index];
  console.log(index, value);
}
// 0 11
// 1 22
// 2 33

配列じゃなくても使えます、というか配列じゃないので使う場面の方が多いかなと思います。

const obj = { a: 11, b: 22, c: 33 };
for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, value);
}
// a 11
// b 22
// c 33

index は文字列

プロパティ名が文字列なのはわかりやすいと思うんだけど、配列の場合でも数値 0, 1, 2, … ではなく、文字列で '0', '1', '2', … が得られます。

仕様的にも、配列の各要素は文字列をキーに格納されているし、添え字アクセスは文字列へ変換してから行われます。

っていう豆知識です。

仕様

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

in 左側

for と同様、以下の三種類。

  • ただの式(宣言なしで i = 0 とか)
  • var 宣言
  • let, const 宣言

いったん for の方を見てください。

for-in の場合は for と違って繰り返し全体のレキシカル環境は生成されないっぽい。

繰り返し毎回のレキシカル環境は for と同様、毎回引き継ぎながら用意される様子。 変数の値を書き換えることなく毎回引き継いで新たに作り直すため、 const が使えます。

ただよくわからないところがあって。

繰り返しの前に一度 in の左側の変数を登録するレキシカル環境を作ってるんだけど、登録終わったら元のレキシカル環境へ戻してます。なんだろこれ。事前に検証してる感じ?

というか TDZ て何の頭文字? ”Tsunami Disaster Zone”? 仕様書中ここにしか出てこないんだけど。

これ↓? ( var と違って巻き上げないよって話。) やっぱり「本番はまだだけど事前にちょっとアレしとこうかなー」みたいな感じ?

in 右側

繰り返し前に評価して、 for-in の場合、繰り返し用にプロパティ名を列挙します。

対象オブジェクトからプロトタイプチェインを順々に辿ってゆき、全てのプロパティを列挙する。ただし、同じ名前のものは一度しか呼ばれない。という感じ。

もしJSで実装するならこういう↓処理らしいよ。

function* EnumerateObjectProperties(obj) {
  const visited = new Set();
  for (const key of Reflect.ownKeys(obj)) {
    if (typeof key === "symbol") continue;
    const desc = Reflect.getOwnPropertyDescriptor(obj, key);
    if (desc) {
      visited.add(key);
      if (desc.enumerable) yield key;
    }
  }
  const proto = Reflect.getPrototypeOf(obj);
  if (proto === null) return;
  for (const protoKey of EnumerateObjectProperties(proto)) {
    if (!visited.has(protoKey)) yield protoKey;
  }
}

列挙可能性

この for-in で出てくるかどうかを「列挙可能性 (enumerability)」と呼んでいるようです。

Object.defineProperty() で作るときに enumerable: false にすると、プロパティはあるけど列挙されなくなります。

const obj = { a: 11, b: 22 };
Object.defineProperty(obj, 'c', {
  enumerable: false, // ←これこれ
  value: 33,
});

console.log(obj.c); // => 33

for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, value);
}
// a 11
// b 22

Object.getOwnPropertyDescriptor() を用いると、どういう設定になっているのか調べられます。

…

const descriptorA = Object.getOwnPropertyDescriptor(obj, 'a');
console.log(descriptorA);
// { value: 11,
//   writable: true,
//   enumerable: true,
//   configurable: true }

const descriptorC = Object.getOwnPropertyDescriptor(obj, 'c');
console.log(descriptorC);
// { value: 33,
//   writable: false,
//   enumerable: false,
//   configurable: false }

ちなみにgetter/setterもこいつらで扱えます。

Own property問題

クラスが導入される以前、ES 5時代のJavaScriptでは関数を使ってコンストラクターを用意していました。その場合 for-in でちょっと困ったことがあります。

prototype に設定したものまで出てきちゃうんです。

function MyClass (options) {
  this.name = options.name;
}
MyClass.prototype.sayHello = function () {
  console.log('Hi my name is ' + this.name + '!');
};

const obj = new MyClass({ name: 'Alice' });
obj.sayHello(); // Hi my name is Alice!

obj.boo = 33;
for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, ':', value);
}
// name : Alice
// boo : 33
// sayHello : function () {
//   console.log('Hi my name is ' + this.name + '!');
// }

prototype へ指定している sayHello が出てますね。これは嬉しくない。

そのインスタンスが自分で持っているプロパティなのか、それとも prototype (やその他)から来ているのかを判断する必要があります。

解決

hasOwnProperty() を使います。

これはオブジェクト自身が指定のプロパティを持っていれば true を返すものです。 obj.hasOwnProperty('sayHello')false になります。

// ...

for (const prop in obj) {
  // prototypeに設定されたもの等は無視
  if (!obj.hasOwnProperty(prop)) {
    continue;
  }

  const value = obj[prop];
  console.log(prop, ':', value);
}

今なら class で大丈夫

です。うん、こっちにしよ。

class MyClass {
  constructor (options) {
    this.name = options.name;
  }

  sayHello () {
    console.log(`Hi my name is ${this.name}!`);
  }
}

const obj = new MyClass({ name: 'Alice' });
obj.boo = 123;

for (const prop in obj) {
  const value = obj[prop];
  console.log(prop, ':', value);
}
// name : Alice
// boo : 33

const desc = Object.getOwnPropertyDescriptor(MyClass.prototype, 'sayHello');
console.log(desc.enumerable); // => false

繰り返し中の操作

削除された場合

呼ばれず無視されます。

A property that is deleted before it is processed by the iterator’s next method is ignored.

反復子 (iterator) の next メソッドにより処理される前に削除されたプロパティは無視される。

追加された場合

えーと「保証されない not guaranteed」てのは、未定義ってことで良いかな。

If new properties are added to the target object during enumeration, the newly added properties are not guaranteed to be processed in the active enumeration.

新しいプロパティが列挙 (enumeration) の途中で対象オブジェクトへ追加された場合、新しく追加されたプロパティが実行中の (active) 列挙内で処理されることは保証されない。

このコード↓で試したところ、繰り返し中には呼ばれませんでした。(手元のChrome, Firefox, Edgeで確認。) もちろん繰り返しの後で確認したら追加されている。

その他

in の右側はゆるい

null とかでもエラーにならない。

for (const i in null) ;

for-of ではエラーです。

順不同

for-in で得られるプロパティ名の順序は未定義 (not specified) です。

The mechanics and order of enumerating the properties is not specified

おしまい

参考