uhyohyo.net

JavaScript初級者から中級者になろう

十一章第四回 Objectとプロパティ

このページの最終更新日:

プロパティの一覧を取得する

ES5の特徴は、オブジェクト、特にプロパティに対する操作が強化されたことです。

まず、オブジェクトに存在するプロパティの一覧を取得することを考えましょう。例えば、


var obj={
  foo:"bar",
  baz:3,
};

というオブジェクトにはfooとbazという2つのプロパティがあります。このように、あるオブジェクトが持っているプロパティを全て調べるということが必要になる機会は少なくないでしょう。

このとき実は、古典的な方法としてはfor-in文があります。これは以下のような形の文です。


for(var 変数名 in オブジェクト){
}

forという名前から察せるように、これはループの一種です。左の変数に、右のオブジェクトが持っているプロパティの名前が入り、全てのプロパティについてループします。例えば次のコードを実行してみましょう。


var obj={
  foo:"bar",
  baz:3,
};

for(var key in obj){
  console.log(key);
}

実行すると、


foo
baz

と出ることでしょう。つまり、変数keyに"foo"が入った状態で1回、"baz"がはいった状態で1回、内部の文が実行されたことになります。

これを使えばプロパティごとに処理をすることが可能です。もちろん、プロパティ名ではなくその値が欲しいならば次のようにします。


for(var key in obj){
  console.log(obj[key]);
}

従来(ES5以前)はプロパティの一覧にアクセスする方法はこれだけでしたが、ES5においては新しい方法が用意されました。

それはObject.keysメソッドです。これは、引数として渡されたオブジェクトが持つプロパティの名前の一覧を配列にして返すメソッドです。ちなみに、このときの順番はfor-in文で列挙される順番と同じことが保証されています。

これを使って上のfor-in文と同じことをしたければ、こうです。


var obj={
  foo:"bar",
  baz:3,
};

var names=Object.keys(obj);

console.log(names);	// ["foo","baz"] のように出力される

names.forEach(function(name){
  console.log(obj[name]);
});

しかし実は、for-in文とObject.keysには1つ決定的な違いがあります。それは継承が関わる場合です。

このサンプルを思い出しましょう。


//敵を作る
function Teki(n){
  this.name = n;
}
//現れる
Teki.prototype.appear = function(){
  console.log(this.name+"が あらわれた!");
};
//攻撃
Teki.prototype.attack = function(){
  console.log(this.name+"のこうげき!ダメージをうけた!");
};

var zako = new Teki("ザコ");

ここで、zakoが持つプロパティはどれでしょうか。


console.log(Object.keys(zako));// ["name"]

これの結果は["name"]です。つまり、Tekiコンストラクタ内で付加されたnameプロパティのみがzakoに存在すると言われています。

しかし、ここですこし考えるべきことがあります。

このオブジェクトzakoがnameプロパティしか持っていないならば、appearメソッドやattackメソッドはどうなるのでしょう。

console.log(zako.appear,zako.attack);

このようにすると、ちゃんと関数が表示されることが確認できますから、appearやattackも、zakoのプロパティとして利用できるはずです。

ここで、nameと、appearやattackの間にある違いは、「オブジェクトの自分自身のプロパティかどうか」という点ですね。appearやattackというのは、nameとは異なりprototypeを介してアクセスしているプロパティなのです。

つまり、appearやattackというのは実はzakoそれ自体がもつプロパティではなく、あくまでTeki.prototypeが持っているプロパティであって、zakoからはそれを間接的に参照できるということです。

この違いは以前はあまり気にするようなことではなかったですが、ES5の時代になって気にすることが増えたような気がします。

さて一方、for-in文で同じことをやってみるとしましょう。


for(var key in zako){
  console.log(key);
}

今度は、nameだけでなくappearやattackも表示されることが確認できたと思います。つまり、for-in文はそのオブジェクト自身がもつプロパティだけでなくprototypeを介して参照するプロパティも列挙するということです。

しかし、理解度の高い方はここで新たな疑問が生じたことでしょう。上で示したサンプルを思い出しましょう。


var obj={
  foo:"bar",
  baz:3,
};

for(var key in obj){
  console.log(key);
}

これはfor-in文ですから、objから参照できるプロパティはprototype経由のものも遡って表示されなければなりません。また、九章第三回で述べたようにこのオブジェクトobjはObjectのインスタンスです。となると、for-in文は当然Object.prototypeをさかのぼって探しに行くはずです。

Object.prototypeがもつプロパティとしてはtoStringなどがあります。次のようにするとその存在を確認できます。

console.log(Object.prototype.toString);

もちろん、さきほどのobjからもtoStringが参照できます。

console.log(obj.toString);

ではなぜ、prototypeまでさかのぼって列挙するはずのfor-in文でも、このtoStringなど(他にもいろいろありますが)は列挙されなかったのでしょうか。それが次の話題です。

プロパティの属性

実は、すべてのプロパティには属性というものが付属しています。属性は、プロパティの性質を決めるものです。

まず紹介するのがenumerable属性です。日本語にすると「列挙可能性」とかそういう感じです。この属性はtrueかfalseかで、enumerableがfalseの属性はfor-in文やObject.keysで列挙されないのです。

我々が普通に作るプロパティは全てenumerableがtrueのプロパティになります。しかし、Object.prototypeが持つメソッドとか、JavaScriptの言語仕様としてすでに存在しているようなプロパティは、基本的に列挙の邪魔にならないようにenumerableがfalseになっています。さっき発生した謎の答えはこれです。

属性は全部で3つあります。writable, enumerable, configurableで、いずれもtrueかfalseかです。

デフォルトは全てtrueで、普通にプロパティに代入するだけでは全てtrueのプロパティしか作れません。

writableは「書き込み可能かどうか」ということで、writableがfalseのプロパティは書き換えられません。また、configurableは「設定可能かどうか」です。この2つの具体的な動作は後述します。

プロパティデスクリプタ

ES5より前のJavaScriptにおいてはこれらの属性は完全に内部的なもので、スクリプト側からどうこうすることはできませんでした。しかしES5ならば、属性に干渉することができます。

属性(など)をいじるには、プロパティデスクリプタを使います。これはプロパティの属性を含む諸々の情報を表すオブジェクトです。これを得るには、Object.getOwnPropertyDescriptorメソッドを使います。


var desc = Object.getOwnPropertyDescriptor(obj, "foo");

第一引数のobjというのは、プロパティを持つオブジェクトで、第二引数はプロパティ名です。つまり、この呼び出しによって、 obj["foo"] についての情報を要求しているわけです。もちろん結果は返り値で戻ってきます。

先ほどの


var obj={
  foo:"bar",
  baz:3,
};

のobjのプロパティfooについて、Object.getOwnPropertyDescriptor(obj,"foo")の結果は次のようなオブジェクトです。


{
  writable: true,
  enumerable: true,
  configurable: true,
  value: "bar"
}

見てわかるように、さきほどの3つの属性についての情報が入っていて、全てtrueであることがわかります。もうひとつvalueプロパティがあって、これがプロパティの値となっています。

なお、これは情報提供用にObject.getOwnPropertyDescriptorによって作られたオブジェクトなので、このオブジェクトの内容を書き換えても何も起こりません。プロパティデスクリプタを変更する方法は後で説明します。その前にもう1つ例を見ましょう。


console.log(Object.getOwnPropertyDescriptor(Object.prototype,"toString"));

Object.prototypeがもつtoStringメソッドです。これの結果はこうなります。


{
  writable: true,
  enumerable: false,
  configurable: true,
  value: function toString() { [native code] }
}

先ほど説明したようにenumerableがfalseになっていることが分かります。valueのfunction toString() { [native code] }というのは、これがtoStringの本体にあたる関数ですが、中身はブラウザの内部処理なのでJavaScriptでは表現できないという意味です。

ちなみに、メソッド名にOwnと入っていることからも分かる通り、オブジェクト自身のプロパティしか得られません。このように、基本的にプロパティをいじるときはそのオブジェクト自身のプロパティが対象になります。

では次に、プロパティデスクリプタを変更する方法を紹介します。それはObject.definePropertyメソッドです。

例えば、writable属性がfalseであるプロパティhogeを、objに追加するには次のようにObject.definePropertyを呼び出します。


Object.defineProperty(obj,"hoge",{
  writable:false,
  enumerable:true,
  configurable:true,
  value:"hogehoge"
});

このように3つの引数をとります。第一引数、第二引数はgetOwnPropertyDescriptorと同じくオブジェクトとプロパティ名ですが、第三引数に新しいプロパティデスクリプタとなるオブジェクトを渡す必要があります。definePropertyを呼び出すと、その新しいプロパティデスクリプタにしたがってプロパティが書き換えられます。

この場合objに新しいプロパティhogeが作られ、obj.hoge"hogehoge"が入っています。

それではwritableの効果を確かめてみましょう。


var obj={
  foo:"bar",
  baz:3,
};

Object.defineProperty(obj,"hoge",{
  writable:false,
  enumerable:true,
  configurable:true,
  value:"hogehoge"
});

console.log(obj.hoge);	//"hogehoge"と表示される

obj.hoge="piyopiyo";	//代入してみる

console.log(obj.hoge);	//結果は…?

結果をみると分かると思いますが、obj.hogeに違うものを代入したはずなのに変わりません。これがwritableをfalseにしたことの効果です。(注:strictモードの場合何も起こらないのではなくエラーが出ますが、それについてはまた今度解説します。)

それでは最後にconfigurableについて解説します。configurableは日本語でいうと「設定可能」といった感じです。

これはすごい力をもっていて、configurableがfalseであるプロパティの属性はいじれません。すなわち、definePropertyしようとするとエラーになります。

加えてwritableもfalseにしておけば、もはやそのプロパティの内容は絶対に不変であることが保証されたようなものです。

例えば、Object.prototypeのようにいじられたら大惨事になるプロパティは、いじれないようにwritableとconfigurableがfalseになっています。

また、実は、ゲッタとセッタについてもdefinePropertyを用いて設定できます。

というのも、その時は既存のオブジェクトにゲッタ・セッタを設定するのに__defineGetter__と__defineSetter__という2つのメソッドを使っていましたが、これは名前が明らかに怪しいことからもわかるように、非標準のメソッドでした。それに対してES5においてdefinePropertyを用いた正式な設定方法が用意されたのです。

ゲッタやセッタを設定したいときは、プロパティデスクリプタのget,setというプロパティに関数を入れてやります。例えば、九章第六回の真偽値しか入れられないプロパティのサンプルは次のように書けます。


var obj = { _a : 0 };
Object.defineProperty(obj,"a",{
  enumerable:true,
  configurable:true,
  get:function(){ return this._a; },
  set:function(n){
    if(n===true || n===false){
      this._a = n;
    }
  }
});

getとsetがある場合writable属性は無視されます。もちろん、ゲッタとセッタがあるのでvalueも要りません。

今までの例ではObject.definePropertyで新しいプロパティを作りましたが、既存のプロパティに対してObject.definePropertyを使うこともできます。このとき、第3引数に渡すオブジェクトはプロパティデスクリプタを部分的に指定することができます。これにより、プロパティの属性を部分的に書き換えることができます。


var obj={
  foo: "bar",
};

Object.defineProperty(obj,"foo",{
  configurable:false,
});

console.log(Object.getOwnPropertyDescriptor(obj, "foo"));

上の例では、Object.definePropertyによりobjのfooプロパティのconfigurableプロパティをfalseに設定しました。他の属性(あるいはvalueプロパティ)は設定されていないのでそのままになります。

新規にプロパティを作るときにこのようにプロパティデスクリプタの一部を省略した場合は、省略された属性はfalseとして扱われます。

その他のメソッド

さて、属性の説明が一段落したところで、その他の関連メソッドを紹介します。

まずObject.definePropertiesです。さっきのdefinePropertyが複数形になっただけであることからもわかるように、複数のプロパティのプロパティデスクリプタを一斉に変更できます。つまりこんな感じです。


Object.defineProperties(obj,{
  foo:{
    value: "bar",
    configurable:true
  },
  bar:{
    value:3,
    enumerable:true
  }
});

第一引数は親となるオブジェクトです。第二引数はオブジェクトで、設定したいプロパティ名をキーにしてプロパティデスクリプタを値としてもつ辞書オブジェクトとなっています。この例ではプロパティfooとbarに対してプロパティデスクリプタを設定しています。

さらに、Object.keysに関連して、Object.getOwnPropertyNamesを紹介します。これは、基本はObject.keysと同じですが、enumerableがfalseのプロパティも列挙するという点で違いがあります。どちらを使うかは場合によりますが、たいていはObject.keysで事足りることと思います。

Objectのインスタンスのプロパティ

さて、今まではObjectが直接持っているメソッドを紹介してきましたが、ここでObjectのインスタンスがもつメソッド(すなわちObject.prototypeがもつメソッド)を1つ紹介します。

まず紹介するのはhasOwnPropertyです。これはプロパティの名前を引数として渡すと、そのプロパティを持っているかどうかを真偽値で返すメソッドです。


//敵を作る
function Teki(n){
  this.name = n;
}
//現れる
Teki.prototype.appear = function(){
  console.log(this.name+"が あらわれた!");
};
//攻撃
Teki.prototype.attack = function(){
  console.log(this.name+"のこうげき!ダメージをうけた!");
};

var zako = new Teki("ザコ");

console.log(zako.hasOwnProperty("name"));	//true
console.log(zako.hasOwnProperty("appear"));	//false
console.log(zako.hasOwnProperty("attack"));	//false

この例を見てわかるように、prototypeチェーンを遡ったりしないことは、Ownと名前に入っていることからも明らかです。また、このメソッドはenumerableがfalseなプロパティであっても正しく判定してくれます。

対照的に、in演算子というのがあります。これも同じようにプロパティが存在するかどうかを確かめる動作をしますが、prototypeチェーンまでさかのぼって探すという違いがあります。


//敵を作る
function Teki(n){
  this.name = n;
}
//現れる
Teki.prototype.appear = function(){
  console.log(this.name+"が あらわれた!");
};
//攻撃
Teki.prototype.attack = function(){
  console.log(this.name+"のこうげき!ダメージをうけた!");
};

var zako = new Teki("ザコ");

console.log("name" in zako);	//true
console.log("appear" in zako);	//true
console.log("attack" in zako);	//true
console.log("foo" in zako);	//false (fooというプロパティは無いので)

このin演算子はfor-in文のinとはあまり関係ないので注意しましょう。

もう1つ、propertyIsEnumerableを紹介します。読んで字の如しというようなメソッド名ですが、hasOwnProperty同様にプロパティ名を渡すと、そのプロパティがenumerableであるかどうかをtrueかfalseで返します。そもそもプロパティがない場合はfalseとなります。

まあこれはあまり使う機会がないかもしれません(getOwnPropertyDescriptorでも情報が取れますし)。

以上で、オブジェクトのプロパティや属性に関する説明は終了です。しかし次回も似たような話が続きます。