LoginSignup
115

More than 5 years have passed since last update.

for-inとObject.keysの違いを正しく知る

Posted at

はじめに

オブジェクトのキーを取得する方法には、次の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.createoriginalというオブジェクトをプロトタイプに設定したobjというオブジェクトを作成出来ます。
Object.createの第2引数は少々特別なオブジェクトの形式になっています。
{a: 1, b: 2, c: 3}の値の部分は{a: {value: 1}, b: {value: 2}, c: {value: 3}}などとする必要があります。
Object.createObject.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.keysfor-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はプロトタイプのオブジェクトのキーも列挙可能であればアクセス出来ます。
そこで、originalc,dも列挙します。
結果forIn1ではa,c,dが表示されています。
そしてforIn2でhasOwnPropertya,c,dから絞り込みをした際に、a,cはobj自身がもっているプロパティのため、a,cが表示されていたと言う事です。
つまり列挙のcはプロトタイプから行われ、hasOwnProperyではobj自身のcから行われているため、異なる対象を判別に使用していたのです。
そのため、結果keysとforIn2で差が発生してしまったのです。

厳密に等価な処理は?

var keys = Object.keys(obj);

for-inhasOwnPropertyを使って厳密に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についての説明

writabletrueにしなければ次に値を代入しても無視され変更する事はできません。つまり読取専用です。
configurabletrueにすると、同じ名称のプロパティを再定義する事が出来ます

proto !== Object.prototype とは?

forIn2の条件式でproto !== Object.prototypeと有るのはObjectの非列挙キーが表示されてしまいます。
結果がむごい事になるので便宜的に対象外にしています

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
115