読者です 読者をやめる 読者になる 読者になる

あと味

たくさん情報を食べて、たくさん発信すると、あとになって味わい深い。

JavaScriptのオブジェクトについて考察してみた

JavaScript

JavaScriptを勉強しているとオブジェクトとはなんぞや?ということがわからなくなってきます。選択肢が増えれば増えるほど。

JavaScriptには、同じように見えて、実は同じではないデータがあります。それらのオブジェクトについて、区別して説明が付けられるように、自分なりに考察してみました。勉強中のアウトプットなので、ここで書いた内容は事実とは大きく外れているものかもしれません。とにかく不明瞭な部分を自分なりに理由づけしたかっただけです。

サンプルコードを試される場合は、FirefoxFireBugにあるコンソールに貼りつけて実行するか、Safariの開発ツールにあるコンソールに貼りつけて実行してください。それがわからない方は console.log の部分を alert に置き換えて確認してください。

話がややこしくなるので、今回はプロパティしか扱っていません。

名称の定義について

オブジェクト
データ構造を持った集合データのこと
プロパティ
オブジェクトに関連づられた変数のこと
クラス
インスタンス化できる関数オブジェクトのこと
インスタンス
コンストラクタを通して、クラスから生成されたオブジェクトのこと
疑似クラス
インスタンス化できない関数オブジェクトのこと*1
疑似インスタンス
疑似クラスの実行を通して生成されたオブジェクトのこと
データ構造を持たない単一のデータのこと

この定義が正しいかどうかはわかりませんが、同じように見えて、実は同じではないデータを区別して説明し、自分で納得するために、上記の名前の定義としました。仕様書を読んで選んだ言葉ではありませんのでご了承ください。

今回はプロパティを定義することを通して、JavaScriptのオブジェクトについて考察します。

プロパティを定義する方法は以下の56通りです。

  1. オブジェクトの中にプロパティを定義する
  2. クラスの中にプロパティを定義する
  3. インスタンスの中にプロパティを定義する
  4. 疑似クラスの中にプロパティを定義する
  5. 疑似インスタンスの中にプロパティを定義する
  6. prototypeプロパティの中にプロパティを定義する

JavaScriptはプロトタイプベースのオブジェクト指向言語なので、インスタンスの中にも、プロパティが定義できます。

サンプルコードと考察

ex1

まずはオブジェクトの中にプロパティを定義してみる。

var o = new Object();
o.prop = "私はプロパティです。";
console.log(o.prop);

// 結果:
// 私はプロパティです。

等価式。

var o = {};
o.prop = "私はプロパティです。";
console.log(o.prop);

// 結果:
// 私はプロパティです。

{} はオブジェクトを作るための簡易表現。オブジェクトリテラルと呼ぶ。

ex2

オブジェクトの定義と同時にプロパティを定義する。

var o = { prop : "私はプロパティです。" };
console.log(o.prop);

// 結果:
// 私はプロパティです。

オブジェクトにプロパティを定義する時の書き方。いわゆるJSONと呼ばれるもの*2

{ プロパティ名 : プロパティの値 }

プロパティが複数ある場合はカンマで区切る。

{ プロパティ1 : プロパティ1の値, プロパティ2 : プロパティ2の値, ... , プロパティn : プロパティnの値 }
ex3

変数にオブジェクトを代入する。

var o = { prop : "私はプロパティです。" };
var obj = o;
console.log(obj.prop);

// 結果:
// 私はプロパティです。

oはobjに代入されている。

ex4

変数にオブジェクトを代入した後、プロパティの値を変更してみる。

var o = { prop : "私はプロパティです。" };
var obj = o;
obj.prop = "私は偽者です。";
console.log(o.prop);

// 結果:
// 私は偽物です。

objを更新するとoも更新される。オブジェクトの代入は、値渡しではなくて、参照渡し。

ex5

このままだと、以下のような処理ができない。

var o = { prop : "私はプロパティです。" };
var obj1 = o;
var obj2 = o;
obj1.prop = "ひとつめのオブジェクトです。";
obj2.prop = "ふたつめのオブジェクトです。";
// ひとつめのオブジェクトです。ふたつめのオブジェクトです。
// と出て欲しい。
console.log(obj1.prop + obj2.prop);

// 結果:
// ふたつめのオブジェクトです。ふたつめのオブジェクトです。

失敗。obj1もobj2も同一の存在になっている。このような時に、クラスが必要になる。クラスは設計書のようなもの。

ex6

new演算子インスタンスが作れるか試してみる。

var o = { prop : "私はプロパティです。" };
var obj = new o();
console.log(o.prop);

// 結果:
// o is not a constructor

oはコンストラクタではないというエラー。

オブジェクト = クラスではない。JavaScriptのクラスは関数オブジェクトで表現できる。クラス = 関数オブジェクト。上の定義通り言うと、クラス = インスタンス化できる関数オブジェクト。

ex7

クラスを作るには、最初の例(ex1)をObjectから、Functionにすれば良い。

var o = new Function();
o.prop = "私はプロパティです。";
console.log(o.prop);

// 結果:
// 私はプロパティです。

等価式。

function o() {};
o.prop = "私はプロパティです。";
console.log(o.prop);

// 結果:
// 私はプロパティです。

さらに等価式。

var o = function() {};
o.prop = "私はプロパティです。";
console.log(o.prop);

// 結果:
// 私はプロパティです。

最後の右辺に代入した function() {} は関数オブジェクトを作るための簡易表現。関数リテラルと呼ぶ。

ex8

クラスの定義と同時にプロパティを定義する。

var o = function() { this.prop = "私はプロパティです。";return this.prop };
console.log(o.prop);

// 結果:
// 私はプロパティです。

thisを使ってpropをoに関連づけている。
ごめんなさい、これはコメントのとおり間違い。コンストラクタを実行してないからundefined。

ex9

変数にクラスを代入してみる。

var o = function() { this.prop =  "私はプロパティです。"  };
var obj = o;
console.log(obj.prop);

// 結果:
// undefined

thisを使って、oにpropを関連付けている。しかし、失敗。

ex10

ex6で失敗した、new演算子インスタンスが作れるか試してみる。

var o = function() { this.prop =  "私はプロパティです"  };
var obj = new o();
console.log(obj.prop);

// 結果:
// 私はプロパティです。

成功。

ex9では、クラスを代入しただけで、インスタンスを作っていなかった。設計書を渡しただけで、実物を作ってなかった。だからundefinedだった。クラスの代入とインスタンスの代入は似ていてもまったく別物。

これはクラスの代入。つまり設計書の代入。

var obj = o;

これはインスタンスの代入。つまり設計書を元にした実物の代入。

var obj = new o();

インスタンスを代入する時の new hoge() という処理をコンストラクタと言う。インスタンスを代入するという言い方は一般的ではない。通常は、インスタンスを生成すると言う。インスタンスを生成する処理のことをコンストラクタと言う。

ex11

変数にインスタンスを代入した後、元のプロパティの値を変更してみる。

var o = function() { this.prop =  "私はプロパティです。"  };
var obj = new o();
o.prop = "私は偽者です。";
console.log(obj.prop);

// 結果:
// 私はプロパティです。

oを更新してもobjは更新されない。

同じ設計書を使って作った実物は、見た目も性能も同じでも、それぞれ別の存在。同じ母ちゃんから生まれた、どこからどう見ても違いがわからない双子がいたとしても、ふたりは別人格。弟が童貞を卒業したからって、兄ちゃんが童貞を卒業したということにはならない。

ex12

ex5で失敗した処理に再挑戦する。ex5では、二つの変数の参照先が、同一だったため失敗していた。

var brother = function() { this.cherry = "はい!まだふたりとも童貞っす!" };
var nichan = new brother();
var otouto = new brother();
otouto.cherry = "童貞?違ぇよw兄貴と一緒にすんなwww";
console.log('兄:' + nichan.cherry + '弟:' + otouto.cherry);

// 結果:
// 兄:はい!まだふたりとも童貞っす!弟:童貞?違ぇよw兄貴と一緒にすんなwww

うん、別人格でしたね。お兄さんはもっと世間を知った方がいい。でもその前に、弟を一発殴っといた方がいいと思います。

ex13

インスタンスにプロパティを定義する。

var o = function() { this.prop =  "私はプロパティです。"  };
var obj = new o();
obj.prop2 = "私はプロパティ2です。";
console.log(obj.prop2);

// 結果:
// 私はプロパティ2です。

インスタンスにもプロパティが定義できる。

ex14

ex8と同じことを、疑似クラスと疑似インスタンスで実現してみる。まずは疑似クラスにプロパティを追加してみる。

var o = function() { return { prop : "私はプロパティです。" } };
console.log(o.prop);

// 結果:
// 私はプロパティです。


これもコメントいただいたとおり間違い。擬似インスタンス化してないからundefined。

ex15

ex9と同様、変数に疑似クラスを代入してみる。

var o = function() { return { prop : "私はプロパティです。" } };
var obj = o;
console.log(obj.prop);

// 結果:
// undefined

失敗。

ex16

ex10と同様、new演算子インスタンスが作れるか試してみる。

var o = function() { return { prop : "私はプロパティです。" } };
var obj = new o();
console.log(obj.prop);

// 結果:
// 私はプロパティです。

うまくいく。実は、この場合new演算子はいらない。oはクラスでないため、インスタンスは作れない。newをつけてもエラーが出ないだけで、instanceofで調べてもobjはoのインスタンスではない。これは、疑似クラスから作られた、疑似インスタンスと考えることにする。

var o = function() { return { prop : "私はプロパティです。" } };
var obj = o();
console.log(obj.prop);

// 結果:
// 私はプロパティです。

等価式。

var o = function() { return { prop : "私はプロパティです。" } };
var obj = o;
console.log(obj().prop);

// 結果:
// 私はプロパティです。

さらに等価式。

var o = function() { return { prop : "私はプロパティです。" } }();
var obj = o;
console.log(obj.prop);

// 結果:
// 私はプロパティです。

疑似クラスは丸括弧によって疑似インスタンス化できる。関数を実行するということ。上記の例の場合、どのタイミングで疑似インスタンス化しても良い。どのタイミングで疑似インスタンス化しても、結果は同じ。

疑似クラスと疑似インスタンスはほんのちょっとの違い。

疑似クラス。

o

疑似インスタンス。

o()
ex17

疑似インスタンスにプロパティを定義する。

var o = function() { return { prop : "私はプロパティです。" } };
var obj = o();
obj.prop2 = "私はプロパティ2です。"
console.log(obj.prop2);

// 結果:
// 私はプロパティ2です。

疑似インスタンスにもプロパティが定義できる。

ex18

ex10のクラスとex16の値オブジェクトは、似ているが違う存在。

var o = function() { this.prop =  "私はプロパティです。"  };
var obj = new o();
console.log(obj instanceof o);

// 結果:
// true

ex10のクラスではtrue。

var o = function() { return { prop : "私はプロパティです。" } };
var obj = o();
console.log(obj instanceof o);

// 結果:
// false

ex16の疑似クラスではfalse。

前者のobjはoクラスのインスタンス、後者のobjは、o疑似クラスを疑似インスタンス化したもの。疑似インスタンスは疑似クラスのインスタンスではなく、あくまで疑似インスタンス

まとめ

JavaScriptオブジェクト指向言語として扱いたい場合、以下のどちらかを利用する。

  1. クラス + インスタンス
  2. 疑似クラス + 疑似インスタンス

言葉の定義は正しくないかもしれないけど、これらは同じように見えて違うものなので、区別して考えようというのが考察した結果です。

追記しました

多数の方にご覧いただき恐縮です。また、ブックマーク、コメント、ブコメ等ありがとうございます。

いただいたコメントやブコメの中でも言及されていましたが、prototypeプロパティを紹介していないのは問題だと思いましたので、本記事に追記することにします。prototypeプロパティを紹介しなかった理由はただひとつ、私がしっかり理解していないからです。試しながら、本を読みながら、理解をしつつ追記していきます。

ex19

prototypeプロパティを使って、プロパティを定義する。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
console.log(o.prop);

// 結果:
// undefined

失敗。やはりコンストラクタインスタンスが必要。

ex20

試しにoの中を確認してみる。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
for (var obj in o) console.log(obj);

// 結果:
// prototype

prototypeプロパティの存在を確認できる。

では、直接アクセスするとどうなるか。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
console.log(o.prototype.prop);

// 結果:
// 私はプロパティです。

成功。プロパティ自体、存在はしているが、o.propで呼び出すためにはコンストラクタインスタンスが必要ということがわかる。

ex20

prototypeプロパティを使って、プロパティを定義し、変数にインスタンスを代入する。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
var obj = new o();
console.log(obj.prop);

// 結果:
// 私はプロパティです。

コンストラクタを実行するとうまくいく。

ex19と同様、直接アクセスするとどうなるか。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
var obj = new o();
console.log(obj.prototype.prop);

// 結果:
// obj.prototype is undefined

ex19で直接アクセスできることが確認できたが、コンストラクタを実行すると、インスタンスの中にはprototypeプロパティが存在しなくなることがわかる。

ex21

試しにobjの中を確認してみる。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
var obj = new o();
for (var p in obj) console.log(p);

// 結果:
// prop

インスタンスの中にはprototypeは存在せず、propのみが存在している。ex20でエラーになった理由が証明できた。

ex22

変数にインスタンスを代入した後、インスタンスのプロパティを変更する。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
var obj = new o();
obj.prop = "私は更新されたプロパティです。";
console.log(obj.prop);

// 結果:
// 私は更新されたプロパティです。

更新される。では、prototypeプロパティの方はどうか。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
var obj = new o();
obj.prop = "私は更新されたプロパティです。";
console.log(o.prototype.prop);

// 結果:
// 私はプロパティです。

更新されていない。インスタンスの更新はprototypeの方には反映されない。

ex23

変数にインスタンスを代入した後、prototypeプロパティの、プロパティを変更する。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
var obj = new o();
o.prototype.prop = "私は更新されたプロパティです。";
console.log(obj.prop);

// 結果:
// 私は更新されたプロパティです。

更新される。しかし、これは奇妙に感じる。ex21でobjにはprototypeプロパティは存在せず、propのみが存在していることを確認したからだ。しかしながら、今、確かにprototypeプロパティへの変更はインスタンスに反映された。つまり、見えなくなってはいるものの、関係は確かに残っている。これがprototypeプロパティの奇異な特徴になる。

ex24

変数にインスタンスを代入した後、インスタンスのプロパティを変更し、その後、prototypeプロパティの、プロパティを変更する。

var o = function() {};
o.prototype.prop = "私はプロパティです。";
var obj = new o();
obj.prop = "私は更新されたプロパティです。";
o.prototype.prop = "私はさらに更新されたプロパティです。";
console.log(obj.prop);

// 結果:
// 私は更新されたプロパティです。

先ほどと同様、最後にprototypeプロパティの変更をしているにも関わらず、反映されていないことが確認できる。つまり、JavaScriptのプロパティは以下の優先順位で採用が決まると言うことがわかる。

インスタンスのプロパティの変更 > prototypeプロパティのプロパティの変更

prototypeプロパティのプロパティは、インスタンスのデフォルト値と考えることができる。これがJavaScriptにおける継承の正体になる。

*1:オブジェクトを返す関数オブジェクト

*2:厳密には少し仕様が違います。[http://ja.wikipedia.org/wiki/JavaScript_Object_Notation:title]