uhyohyo.net

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

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

今回から、第十一章においては、ECMAScript5に基づいた説明をしていきます。

これは、現在(2012年)一番新しいJavaScriptの仕様を規定するものです。ECMAScript5は2010年頃に登場し、現在までに主要ブラウザの対応がほぼ完了しています(IE除く)。

また、ECMAScript Harmonyと呼ばれるさらに進化したECMAScript(必然的にJavaScriptに取り入れられるでしょうが)が策定中です。

それで、今回紹介するのはECMAScript5において登場した、オブジェクトやプロパティについてのあれこれを紹介します。これらによって、JavaScirptのオブジェクトをより深く詳細に操れるようになりました。

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

例えば、

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

というようにしてオブジェクトを作ったとき、このオブジェクトにはfooとbazという2つのプロパティがあります。

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

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

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

の形で、左の変数に、右のオブジェクトが持っているプロパティの名前が入り、全てのプロパティについてループします。例えば次のコードを実行してみましょう。

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]);
}
      

しかし、ECMAScript5においては新しい方法が用意されました。

それには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"]です。つまり、Tekiコンストラクタ内で代入されたnameプロパティのみが存在すると言われています。

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

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

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

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

ここで、nameと、appearやattackの間にある違いは、「オブジェクトの自分自身の(own)プロパティかどうか」という点ですね。

appearやattackというのは、直接「zako.appear=...」のようにして代入したものではないですね。以前やった通り、prototypeを介してアクセスしているのです。

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

この違いは、以前はあまり気にするようなことではなかったですが、ECMAScript5の時代になって気にすることが増えたような気がします。この違いを頭に入れておきましょう。

さて一方、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を遡っているということです。ここで、{ 〜 }で作られるようなただのオブジェクトは、一章第一回で解説したようにObjectのインスタンスですから(つまりここで紹介しているObject.keysはコンストラクタであるObjectがメソッドを持つというわりと珍しい例です)、当然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で、普通にプロパティに代入するだけでは全てtrueのプロパティしか作れません。

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

プロパティデスクリプタ

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

属性(など)をいじるには、プロパティデスクリプタを使います。これはただのオブジェクトです。これを得るには、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プロパティがあって、これがプロパティの値となっています。

試しにもうひとつやってみましょう。

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(obj,"hoge",{
  writable:false,
  enumerable:true,
  configurable:true,
  value:"hogehoge"
});

このように3つの引数をとります。第一引数、第二引数はgetOwnPropertyDescriptorと同じですが、第三引数に新しいプロパティデスクリプタが追加されます。definePropertyを呼び出すと、その新しいプロパティデスクリプタにしたがってプロパティが書き換えられます。

それでは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,enumerable,configurableが全てfalseになっています。

ちなみに、definePropertyのときにwritable,enumerable,configurableが省略された場合falseとして扱われます。

また、九章第六回で紹介したゲッタとセッタについても、このdefinePropertyを用いて設定できます。

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

ゲッタやセッタを設定したいときは、プロパティデスクリプタのget,setというプロパティに関数を入れてやります。例えば、

var obj = { _a : 0 };
obj.__defineGetter__("a", function(){ return this._a; });
obj.__defineSetter__("a", function(n){
  if(n===true || n===false){
  this._a = n;
  }
});

このサンプルをdefinePropertyで書きなおしたならば、次のようにします。

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

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

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

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

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

紹介するのは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
      

for-in文のinとは関係ないので注意しましょう。

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

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