はじめに
今回はJavascriptでインスタンス(オブジェクト)生成からprototypeを使ってメソッド定義を学んで行きます。
前提
Javascriptにはインスタンスという概念は存在するものの、クラスという概念はありません。代わりにプロトタイプを使ってオブジェクト生成を行なっていきます。プロトタイプとはあくまでクラスではなく、「オブジェクトの基となるオブジェクト」です。Javascriptはこのプロトタイプを使ってオブジェクトを生成することから「プロトタイプベースのオブジェクト指向」と言われるそうです。
1, コンストラクターで初期化する
下記のコードでは、関数リテラルの内部にプロパティを二つ、メソッドを一つ定義しています。一見、関数リテラルを変数に代入しているだけと思われますが、これでJavascriptでのクラス定義が完了です。
let member = function(lastName,firstName){
this.firstName = firstName; // プロパティの定義
this.lastName = lastName; // プロパティの定義
// メソッドの定義 (厳密にはメソッドがなく関数リテラルをプロパティに渡すことでメソッド的役割になる)
this.getName = function(){
return this.lastName + '' + this.firstName;
}
}
// コンストラクターで初期化。インスタンスを生成。ここでは member1
let member1 = new member('山田','太郎');
console.log(member1.getName()); // => 山田 太郎
console.log(member1.firstName); // => 山田 (プロパティも参照可)
// 関数内の this はコンストラクターによって生成されるインスタンスを指す。ここでは member
変数memberには関数リテラルが代入されています。それをコンストラクターで初期化することでオブジェクトの生成ができます。let member1 = new member('山田','太郎');
初期化する際に引数('山田','太郎'
)を渡す事でクラスのfirstName
、lastName
にそれぞれ値が渡されます。これらがthis.firstName
、this.lastName
に代入されます。このthis
は大体、予想できると思いますが変数member
を指しています。
このmemberを初期化することでコピーされオブジェクトが生成される訳なのでオブジェクトにも勿論、これらのプロパティを持っています。
同様にクラス内に定義されているgetName
メソッドもオブジェクトの中にコピーされています。クラス内に関数リテラルを再度、定義していますがこれでJavascriptにおけるメソッドの定義ができます。
補足(thisの参照先)
もし以下のようなコンストラクターで初期化しなかった場合の挙動について見てみます。
let member = function(firstName,lastName){
this.firstName = firstName;
this.lastName = lastName;
getName = function(){
return this.lastName + ' ' + this.firstName;
}
}
let m = member('田中','太郎'); // new member('田中','太郎') となっていない。
// ここで member オブジェクトは生成されていない。
console.log(firstName); // => 田中(グローバルオブジェクトの firstName)
console.log(m.firstName); // Cannot read property 'firstName' of undefined
console.log(m.getName()); // Uncaught TypeError: Cannot read property 'getName' of undefined
当然、上記のlet m = member('田中','太郎');
のように初期化しないままだとインスタンスは生成されず、変数m
からプロパティやメソッドを呼び出そうとしても定義されていない為、エラーとなります。
しかし、console.log(firstName);
はしっかり田中と出力されています。
実はこれはグローバル変数firstName
を出力しています。グローバル変数に定義した覚えがありませんが実は、let m = member('田中','太郎');
の処理の際に関数リテラルmemberに引数が渡され、this.lastName
、this.firstName
に代入されるのですが、ここで使われているthisが指しているのはグローバルオブジェクトを指しています。なのでこの関数リテラルmemberを呼び出し、値を渡した時点でグローバル変数firstName
とlastName
が生成されてしまっていたのです。
thisを使う際に気をつけるべきなのが使う箇所によって参照先が変わってしまうということです。
以下、 thisを使う箇所とその際の参照先を纏めておきます。
1、トップレベル(関数の外) : グローバルオブジェクト
2、関数 : グローバルオブジェクト
3、call/apply メソッド : 引数で指定されたオブジェクト
4、コンストラクター : 生成したインスタンス
5、メソッド : 呼び出し基のオブジェクト
2, メモリを意識したメソッド定義の方法
上記まで紹介してきた方法でクラスを定義し、内部にプロパティとメソッドを定義する方法だと挙動に問題は無いものの、消費メモリの観点からみて問題があります。クラス内部にメソッドを定義して、そのクラスを基にオブジェクトを生成した際にその内部全てがコピーされ、一意の独立したオブジェクトが生成されます。
しかし、オブジェクトによっては使わないメソッドが存在するケースがある為、使わないメソッドを都度、コピーしてしまってはメモリの無駄です。一つ、インスタンスを生成するだけならいいですが、これが大量のインスタンスを生成した時には、その分のメモリが無駄になってしまいます。
それを防ぐためにメソッドを定義する際には以下のようにプロトタイプを使って定義しましょう。
let member = function(lastName,firstName){
this.lastName = lastName;
this.firstName = firstName;
}
// プロトタイプとして定義するメンバー(メソッド)を指定
member.prototype = {
getName : function(){
return this.lastName + ' ' + this.firstName;
},
sayHello : function(){
return 'Hello, ' + this.lastName;
}
}
member1 = new member('鈴木','二郎');
console.log(member1.getName()); // => 鈴木 二郎
console.log(member1.sayHello()); // => Hello, 鈴木
メソッドを定義する際には上記のmember.prototype = {メソッド定義}
のように定義しましょう。
このようにすることで、インスタンス化されたオブジェクト内部にはメソッドはコピーされず、インスタンスの元となるオブジェクト、(この場合のmember)の内部にprototypeプロパティとして格納されます。
インスタンス化されたオブジェクト(この場合のmember1)がメソッドgetName
、sayHello
を呼び出した際にはインスタンスの元となるオブジェクト(この場合のmember)の内部に存在するprototypeまで参照しに行ってくれます。
補足(プロトタイプの隠蔽)
あまり、使用例が私には思いつかないのですがプロトタイプで定義したメソッドの処理内容を上書きしたり新たに追加したりできるのもプロトタイプの利点です。以下のコードをみてください。
let member = function(lastName,firstName){
this.lastName = lastName;
this.firstName = firstName;
}
// プロトタイプとして定義するメンバー(メソッド)を纏めて指定する方法
member.prototype = {
getName : function(){
return this.lastName + ' ' + this.firstName;
},
sayHello : function(){
return 'Hello, ' + this.lastName;
}
}
member1 = new member('鈴木','二郎');
console.log(member1.getName()); // => 鈴木 二郎
console.log(member1.sayHello()); // => Hello, 鈴木
member2 = new member('鈴木','二郎');
member2.getName = function(){
return '上書き'
}
console.log(member1.getName()); // => 鈴木 二郎
console.log(member2.getName()); // => 上書き
member1
に加えて全く同じ引数でmember2
をインスタンス化して生成しました。
生成した後、member2.getName = function(){return '上書き'}
というメソッドをmember2に追加します。
この状態で、console.log(member2.getName());
でgetName
メソッドを呼び出すと狙い通り、上書き処理が出力されました。勿論、member1
には上書きしたメソッドは存在しないのでしっかり、プロトタイプを参照しに行ってくれてます。
このようにプロトタイプで定義したメソッドを上書きすることを、プロトタイプの隠蔽
と言います。このようにリアルタイムで定義を上書きしたりすることでより柔軟にオブジェクト毎に処理を定義することができました。