#JS学習シリーズの目的
このシリーズは、私ジャックが学んだJavaScriptのメカニズムについてアウトプットも兼ねて、
皆さんと知識や理解を共有するためのものです。
(理解に間違いがあればご指摘いただけると幸いです)
#コンストラクター関数とは
「新しくオブジェクトを作成するための雛形となる関数」
コンストラクター関数は、一般的な関数とは異なり、専用のnew
演算子を使ってオブジェクトを生成します。
function A() {
this.prop = 0;
}
const obj = new A();
上記のコードのように、
new
演算子で作成したオブジェクトをインスタンスと言い、new
演算子でインスタンスを作成することをインスタンス化と言います。
※コンストラクター関数では、慣例として関数名の先頭を大文字で書きます。
#prototypeとは
「オブジェクトに存在する特別なプロパティー」
「コンストラクター関数と合わせて使用」
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.hello = function() {
console.log('hello ' + this.name);
}
const bob = new Person('Bob', 18);
const tom = new Person('Tom', 33);
const sun = new Person('Sun', 20);
bob.hello(); /*hello Bob*/
tom.hello(); /*hello Tom*/
console.log(bob);
上記のコードでは、
prototype
プロパティにhello
メソッドを追加して、それをnew
演算子でインスタンス化したオブジェクトから呼び出しています。
よって、実行結果はそれぞれコメントした内容となります。
ここで、console.log(bob)
の結果を見てみると、
上記の画像のように、インスタンスの中に存在する__proto__
の中に、先ほどprototype
に追加した
hello
メソッドが格納されています。
このことから、インスタンス化した際にはprototype
の参照が__proto__
にコピーされるということが分かります。
↑は、非常に重要なので理解しておきましょう!
##なぜprototypeを使うのか
前述したように、コンストラクター関数でメソッドを定義する際は、prototype
を使って定義します。
ただ、prototype
を使わなくても直接コンストラクター関数に定義することはできます。
では、なぜprototype
を使って定義するのでしょうか?
それは、「メモリの効率化」のためです。
下記のコードを見てみましょう。
function Person1(name, age) {
this.name = name;
this.age = age;
this.hello = function() {
console.log('hello ' + this.name);
}
}
const bob = new Person1('Bob', 18);
bob.hello(); /*hello Bob*/
function Person2(name, age) {
this.name = name;
this.age = age;
}
Person2.prototype.hello = function() {
console.log('hello ' + this.name);
}
const tom = new Person2('Tom', 33);
tom.hello(); /*hello Tom*/
上記のコードでは、
Person1
ではコンストラクター関数に直接メソッドを定義して、Person2
ではprototype
にメソッドを定義しています。
どちらも、インスタンス化してメソッドを呼び出した際の実行結果は同じです。
しかし、この2つの違いは、
- Person1のインスタンスでは、メソッドそのものをメモリ空間にコピーして作成している
- Person2のインスタンスでは、
prototype
に定義したメソッドの参照を__proto__
にコピーしている
つまり、Person1
の書き方だと、インスタンスを生成する度にhello
メソッドを追加する必要があるので、余分なメモリを消費することになります。
一方、'Person2'の書き方だと、インスタンスを生成した際にprototype
の参照をコピーするため、オブジェクトに格納されているメソッドの参照先は全て一致するので、余分なメモリを消費せずに効率的にプログラムを動かすことができます。
そのため、このprototype
というオブジェクトは、JavaScriptの仕組みを支える重要な技術となります。
#new演算子
まず、new
演算子とは
「コンストラクター関数からインスタンスを作成するために使用する演算子」
これまでもnew
演算子は使ってきましたが、new
演算子でコンストラクター関数からインスタンスを作成する際、
コンストラクター関数の戻り値によって動きが変わってきます。
- コンストラクター関数の戻り値がオブジェクトの場合は、コンストラクターが返す
return
のオブジェクトを新しいインスタンスオブジェクトとして呼び出し元に返す - コンストラクター関数の戻り値がオブジェクト以外、もしくは戻り値の
return
が定義されていない場合は、コンストラクターのprototype
の参照をインスタンスの__proto__
にコピーして、コンストラクターで使用している'this'を呼び出し元に返却する(※インスタンスを'this'の参照先としてコンストラクター関数を実行)
function F1(a, b) {
this.a = a;
this.b = b;
return {};
}
const instance1 = new F1(1, 2);
console.log(instance1); /*{}*/
function F2(a, b) {
this.a = a;
this.b = b;
return 1;
}
const instance2 = new F2(1, 2);
console.log(instance2); /*{a: 1, b: 2}*/
上記のコードでは、
F1
では、戻り値がオブジェクトなので、new
演算子でインスタンス化した時、return
のオブジェクトをインスタンスオブジェクトとして返すので、結果は(今回の場合){}
(空のオブジェクト)となります。
F2
では、戻り値がオブジェクト以外のプリミティブ値なので、'new'演算子でインスタンス化した時、(今回はprototype
でメソッドを定義していませんが)'prototype'の参照を__proto__
にコピーして、'this'の参照するオブジェクトはインスタンス(今回の場合はinstance2
)になります。
#instanceofとは
「どのコンストラクターから生成されたオブジェクトかを確認する」
function F(a, b) {
this.a = a;
this.b = b;
}
F.prototype.c = function() {}
const instance = new F(1,2);
console.log(instance instanceof F); /*true*/
console.log(instance.__proto__ === F.prototype) /*true*/
上記のコードでは、
console.log(instance instanceof Object)
で、instance
がF
のインスタンスなので、true
が返ってきます。
ちなみに、console.log(instance.__proto__ === F.prototype)
で、instance.__proto__
とF.prototype
が同じ参照先のオブジェクトなのでこれもtrue
が返ってきます。
##instanceofの使用例
instanceof
はプロトタイプチェーンをさかのぼって検証を行うので、例えば次のように関数に渡す引数が配列かオブジェクトかで条件分岐して関数を実行することもできます
(※プロトタイプチェーン・・・プロトタイプの多重形成をプロトタイプチェーンと言う)
function fn(arg) {
if(arg instanceof Array) {
arg.push('value');
} else if (arg instanceof Object) {
arg['key'] = 'value';
}
console.log(arg)
}
fn([]) /*["value"]*/
#まとめ
いかがでしたでしょうか。
コンストラクター関数は、ES6から導入されたクラス構文の基礎となる部分なので、しっかり理解しておきましょう!