はじめに
オブジェクトのキーを取得する方法には、次の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の非列挙キーが表示されてしまいます。
結果がむごい事になるので便宜的に対象外にしています