JavaScriptのコンストラクタを理解する(シリーズその3)

More than 3 years have passed since last update.

「必要なのはJavaScript言語仕様書とブラウザーコンソールだけ!」シリーズ、その3。


コンストラクタ(constructor)とは?

JavaScript言語仕様によると、constructorとは、


function object that creates and initialises objects

(訳:オブジェクトを作成し初期化するfunctionオブジェクト)

NOTE The value of a constructor’s “prototype” property is a prototype object that is used to implement inheritance and shared properties.

(訳:constructorの「prototype」という名前のプロパティの値はprototypeオブジェクトであり、継承とプロパティ共有に利用される)


この定義は逆説的に言うと、『「protoype」という名前のプロパティを持つオブジェクト=コンストラクタだ』と言っている。

ということは、あるfunctionオブジェクトがコンストラクタであるかどうかの判定は、例えばmyObject.prototyp という風にコンソールに打ち込んで、何らかの値が返ってくるかどうかを調べれば良い。


built-inオブジェクトがコンストラクタであるかどうかを確認する

Object、Function、Boolean、String、それからついでにArrayとNumberというbuilt-inオブジェクトがコンストラクタなのかどうか、コンソールに入力して確認してみる。

Object.prototype

Function.prototype
Boolean.prototype
String.prototype
Array.prototype
Number.prototype

結果:

2015-12-05 14_23_54-Start.png

全てのbuilt-inオブジェクトに「prototype」という名前のプロパティが存在する。つまり、ここで試したオブジェクトはすべてコンストラクタである。

要点

「prototype」という名前のプロパティがあるオブジェクトはコンストラクタである。


コンストラクタではないfunction オブジェクトってあるの?

ある。例えば文字を数として解釈するためのfunctionである parseInt が一例。

parseInt('23')

parseInto.prototype

結果:

2015-12-05 14_28_58-Start.png

まず、'23'という文字を数として読み取るという振る舞いから、parseIntがfunctionだと分かる。

しかし、prototypeという名前のプロパティを要求すると

undefined (定義されていません)

と返ってきた。そのような名前のプロパティは無いとのこと。

よってparseIntはfunctionだがコンストラクタではない。


コンストラクタとコンストラクタが作ったオブジェクトの切っても切れない関係

子供が親の名前を憶えているように、オブジェクトも自分を生み、初期化してくれたコンストラクタ(functionオブジェクト)を覚えている。

var x = Object()

x.constructor
x.constructor == Object

結果:

2015-12-05 14_42_38-Start.png

x.constructor == Objecttrueを返していることから分かるように、オブジェクトの「constructor」という名前のプロパティは、自分を生んだコンストラクタを参照している。

要点

オブジェクトの「constructor」という名前のプロパティは、そのオブジェクトを作ったコンストラクタを参照している。


コンストラクタが誰だったかを覚えておくと何か得なことでも?

この投稿の冒頭にあった言語仕様の一節:


(訳:constructorのprototypeという名前のプロパティの値はprototypeオブジェクトであり、インヘリタンスとプロパティの共有に利用される)


を思い出してほしい。2つの利用目的を記している:(1)インヘリタンス(継承)と(2)プロパティの共有。

インヘリタンスは後述するとして、二つ目の目的「プロパティの共有」の様子を確認するため、以下のようにコンソールに入力してみる

var x = Object();

var y = Object();
x.constructor.prototype.test = 'hello'
x.test
y.test
var z = Object()
z.test


  1. xという名前のObjectオブジェクトを作る

  2. yという名前のObjectオブジェクトを作る

  3. xを作ったコンストラクタの「prototype」というオブジェクトに新たにtestという名前のプロパティを加える

  4. x.testの値は?

  5. y.testの値は? (共有しているなら同じ値のはず)

  6. 別に、zという名前のObjectオブジェクトを作る

  7. z.testの値は?

結果:

2015-12-05 15_10_36-Start.png

3つのオブジェクト(x、y、そしてz)は全て test というプロパティに対する値を返し、なおかつ値は同じだった。

これはxのコンストラクタのprototypeに加えた test というプロパティが自動的に共有されたからだ。なぜならyとzも同じコンストラクタで作られたので同じprototypeを共有しているから。

ここで混乱の原因になりやすい点が二つ。

まず一点目は、とくにC++ などでプロパティ(C++の世界では通常「メンバー」と呼ばれるもの)の共有を直近の親のプロパティを介して行う癖がついている人(私も含む)は混乱しやすい。

class Animal { public: int test; };

class Cat : protected Animal { };

のような場合である。この場合、testはCatクラスのベースになっているAnimalのメンバーであり、新たにDogクラスがAnimalから派生すれば、CatとDogがtestを共有する。

JavaScriptでもオブジェクトからすぐに辿っていけるconstructorオブジェクトのプロパティであれば、C++やC#やJavaに慣れた人にでもごく自然だったであろう。

しかし実際はそうではなく、以下の要点のようになる。

要点

JavaScriptでは、同じコンストラクタで作成されたオブジェクトが、そのコンストラクタのプロパティの一つであるprototypeのプロパティを共有する。

仕様にあるこの図はprototypeを通した共有の仕組みを表している。constructorがCF,constructorのprototypeがCFpで表されており、そのconstructorで作成された5つのオブジェクトがCFpを参照しているのが分かる。

では、同じプロパティ名ではあるけれど、値を共有したくない場合はどうすればいいのか?


俺は自分だけのプロパティの値が欲しい、という人のために

例えば、zが、自分だけは独立した値が欲しい。xとyとの共有は拒否したい、と言い出した場合。

同じプログラムに対し、下のように入力し、結果を見てみる。

z.test

z.test = 'testtest'
x.test
z.text

ここでは2行目で、zが独自の値testtestを持つようになったことに注目。

さてxはこの影響を受けたかどうか・・・

結果:

2015-12-05 15_27_16-Start.png

xは影響は受けていない。

xはあくまでもconstructor.prototype(暗示的に関連づけられたprototype)のtestプロパティを使っている。なぜならxが自分自身でtestという名前のプロパティを定義していないから。

その点zは独自にtestというプロパティを定義したので、testプロパティを要求されたとき、自分が定義したものを何よりも優先的に使う。この仕組みはC++などのオーバーライディング(overriding)に似ている。


ところで・・・new を付けなくてもオブジェクトは作れるの?

これまでのところ、オフジェクトをいくつか作ってきたが、お決まりの予約語「new」が一度も出てきていないことに気付いただろうか?

その4につづく