繰り返しにもいろいろあるよ。
for
,while
→ for文を仕様からじっくり見てみる。あとwhileとか。(配列とかおれおれAdvent Calendar2018 – 13日目)for-in
for-of
→ 別稿for-await-of
→ 別稿forEach()
→ 別稿
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
おしまい
参考
- for…in – JavaScript | MDN
- プロパティの列挙可能性と所有権 – JavaScript | MDN
- Enumerability and ownership of properties | MDN
- Object.prototype.hasOwnProperty() – JavaScript | MDN
- ECMAScript® 2018 Language Specification
- 13.7.5 The for-in, for-of, and for-await-of Statements
- 13.7.5.12 Runtime Semantics: ForIn/OfHeadEvaluation ( TDZnames, expr, iterationKind ) …
for-in
はiterationKind
がenumerate
- 13.7.5.13 Runtime Semantics: ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind, labelSet [ , iteratorKind ] )
- 13.7.5.15 EnumerateObjectProperties ( O )
- Why is there a “temporal dead zone” in ES6?