はじめに
オブジェクトのキーを取得する方法には、次の2つの方法が知られています。
それは、for-in
で繰り返し処理による取得とObject.keys
による取得です。
実はこれいつでも同じ処理をするものだと思っていませんか?
for (var p in obj) {
if (obj.hasOwnProperty(p)) {
//do something
}
}
Object.keys(obj).forEach(function (p) {
//do something
})
実は常に同じとは限りません
さてクイズです。
どのような時に同じにならないのかを考えてみてください。
正解はこのあと
・
・
・
早速、検証コードから見る
次のコードはまだ理解しなくいいですが、まずは実行した結果の違いをよく見てください。
function showProperties (obj) {
// 検証1 - keys
// Object.keysを使う
var keys = Object.keys(obj);
console.log('keys: ', keys);
// 検証2 - forIn1
// 列挙可能なキーを取得する
var forIn1 = [];
for(var p1 in obj) {
forIn1.push(p1);
}
console.log('forIn1:', forIn1);
// 検証3 - forIn2
// 列挙可能なキーからプロトタイプのキーを除外する
var forIn2 = [];
for(var p2 in obj) {
if (obj.hasOwnProperty(p2)) {
forIn2.push(p2);
}
}
console.log('forIn2:', forIn2);
// 検証4 - names1
// 列挙出来ないキーも取得する
var names1 = Object.getOwnPropertyNames(obj);
console.log('names1:', names1);
// 検証5 - names2
// プロトタイプの列挙出来ないキーも取得する
var names2 = Object.getOwnPropertyNames(obj);
var proto = Object.getPrototypeOf(obj);
if (proto && proto !== Object.prototype) {
names2 = names2.concat(Object.getOwnPropertyNames(proto));
}
console.log('names2:', names2);
console.log(); // 空行
}
var obj;
// --------------------------- pattern 1
obj = {
a: 1,
b: 2,
c: 3
};
showProperties(obj);
// keys: [ 'a', 'b', 'c' ]
// forIn1: [ 'a', 'b', 'c' ]
// forIn2: [ 'a', 'b', 'c' ]
// names1: [ 'a', 'b', 'c' ]
// names2: [ 'a', 'b', 'c' ]
// --------------------------- pattern 2
function Fn () {
this.a = 1;
this.b = 2;
}
Fn.prototype.c = 3;
obj = new Fn();
showProperties(obj);
// keys: [ 'a', 'b' ]
// forIn1: [ 'a', 'b', 'c' ]
// forIn2: [ 'a', 'b' ]
// names1: [ 'a', 'b' ]
// names2: [ 'a', 'b', 'constructor', 'c' ]
// --------------------------- pattern 3
var original = {
c: 4,
d: 5
};
obj = Object.create(original, {
a: {value: 1},
b: {value: 2},
c: {value: 3}
});
showProperties(obj);
// keys: []
// forIn1: [ 'c', 'd' ]
// forIn2: [ 'c' ]
// names1: [ 'a', 'b', 'c' ]
// names2: [ 'a', 'b', 'c', 'c', 'd' ]
// --------------------------- pattern 4
var original = {
c: 4,
d: 5
};
obj = Object.create(original, {
a: {value: 1, enumerable: true},
b: {value: 2},
c: {value: 3}
});
showProperties(obj);
// keys: [ 'a' ]
// forIn1: [ 'a', 'c', 'd' ]
// forIn2: [ 'a', 'c' ]
// names1: [ 'a', 'b', 'c' ]
// names2: [ 'a', 'b', 'c', 'c', 'd' ]
解説
クイズの正解は、結果のkeysとforIn2に差があるところで確認できます。
実はもう一つオブジェクトのキーを取得するのにObject.getOwnPropertyNames
と言うのが存在します。これについては結果names1,names2で確認できます
これらの3つのキーの取得の方法の違いについて正しく理解します。
まずは、pattern1では全ての結果が同じになるようにkeys,forIn1,forIn2,names1,names2を揃えてました。
ここからどう違いが出るのかを見ます
pattern 2
関数をnew
した時に生成されるオブジェクトを対象にしています
これによりプロトタイプが設定されたオブジェクトになっています
forIn1にありforIn2に無いc
は、プロトタイプのキーのためhasOwnProperty
の判定ではfalse
になります。
これは多くの書籍で説明されているので理解しやすいと思います。
まだkeysとforIn2に違いはありません。
names1,names2については次で詳しく解説します
pattern 3
ではnew
を使用しないでプロトタイプを指定したらどうなるでしょうか?
Object.create
でoriginal
というオブジェクトをプロトタイプに設定したobj
というオブジェクトを作成出来ます。
Object.create
の第2引数は少々特別なオブジェクトの形式になっています。
{a: 1, b: 2, c: 3}
の値の部分は{a: {value: 1}, b: {value: 2}, c: {value: 3}}
などとする必要があります。
Object.create
をObject.defineProperty
に置き換えて記述すると次のようになります。
obj = {};
obj.__proto__ = original;
Object.defineProperty(obj, 'a', {value: 1});
Object.defineProperty(obj, 'b', {value: 2});
Object.defineProperty(obj, 'c', {value: 3});
keysとforIn2で違いが出たぞ!
さて、pattern3で結果keys,forIn2に違いが出ました。
実は、Object.defineProperty
の第3引数の{value: 1}
はオブジェクトの1
を単純に値に設定する事とは異なります
つまり次のコードは上のコードとは処理が違います。
obj = {};
obj.__proto__ = original;
obj.a = 1;
obj.b = 2;
obj.c = 3;
もし完全に同じとしたい場合は次のようにします。
obj = {};
obj.__proto__ = original;
Object.defineProperty(obj, 'a', {value: 1, enumerable: true, writable: true, configurable: true});
Object.defineProperty(obj, 'b', {value: 2, enumerable: true, writable: true, configurable: true});
Object.defineProperty(obj, 'c', {value: 3, enumerable: true, writable: true, configurable: true});
とする必要があります。
javascriptではオブジェクトのプロパティにpublic
,private
などアクセス修飾子が存在しません。
そのかわり、列挙出来る出来ないの属性を持っています。それが、enumerable
になります。
列挙できるプロパティはObject.keys
やfor-in
でキーを取得する事が出来ます。
では、列挙できないプロパティの存在を知ることは出来るでしょうか?
はい、できます。
そんなときはObject.getOwnPropertyNames
です。
Object.getOwnPropertyNames
は非列挙も取得するObject.keys
と思ってください。
そのため、結果のnames1,names2ではa,b,cが取得出来たのです。
(※writable,configurableについての説明は補足で最後にしています)
前置きはいいから。で、なぜ違いがでたの?
それはpattern4の結果をみるとよくわかります。
pattern4ではa
だけ、列挙出来るようにenumerable
を追加しています。
そのため、for-in
ではまず列挙可能なキーとしてa,b,c
のうちa
だけにアクセス出来ます。
さらにfor-in
はプロトタイプのオブジェクトのキーも列挙可能であればアクセス出来ます。
そこで、original
のc,d
も列挙します。
結果forIn1ではa,c,d
が表示されています。
そしてforIn2でhasOwnProperty
でa,c,d
から絞り込みをした際に、a,c
はobj自身がもっているプロパティのため、a,c
が表示されていたと言う事です。
つまり列挙のc
はプロトタイプから行われ、hasOwnPropery
ではobj
自身のc
から行われているため、異なる対象を判別に使用していたのです。
そのため、結果keysとforIn2で差が発生してしまったのです。
厳密に等価な処理は?
var keys = Object.keys(obj);
をfor-in
とhasOwnProperty
を使って厳密にObject.keys
と同じ処理を記述すると次のようになります
var keys = [];
for(var p in obj) {
if (obj.hasOwnProperty(p)) {
var dest = Object.getOwnPropertyDescriptor(obj, p);
if (dest.enumerable) {
keys.push(p);
}
}
}
Object.getOwnPropertyDescriptor
はプロパティの定義の取得し列挙出来るかどうかを確認出来ます。
面倒くさいですね。素直にObject.keys
を使いましょう。
補足・注意
今回の記事はECMAScript5以上であることで説明しています。
obj.__proto__
は非標準のプロパティです。動作しないブラウザがあります
writable,configurableについての説明
writable
をtrue
にしなければ次に値を代入しても無視され変更する事はできません。つまり読取専用です。
configurable
をtrue
にすると、同じ名称のプロパティを再定義する事が出来ます
proto !== Object.prototype とは?
forIn2の条件式でproto !== Object.prototype
と有るのはObject
の非列挙キーが表示されてしまいます。
結果がむごい事になるので便宜的に対象外にしています