最近JavaScriptを触り始めた人は、クラスから学び始める人は少なくないと思います。
ですが、どれだけクラスの知識があってもプロトタイプをちゃんと理解していないといつかボロが出ます。
プロトタイプを理解して初めてクラスを使いこなせると言えるので、この機会に学んでいきましょう。
#コンストラクタ関数
複数のオブジェクトを使う際にコンストラクタ関数を使います。
同じようなオブジェクトを大量に生産する際や、決められたテンプレートを生産する際の型みたいなものです。
function Person(first, last) {
this.first = first;
this.last = last;
}
let name1 = new Person('tanaka', 'taro')
下記は、自己紹介文を出力できるthis.introduceプロパティ
を追加しています。
function Person(first, last) {
this.first = first;
this.last = last;
this.introduce = function() {
console.log('My name is ' + first + ' ' + last)
}
}
let person1 = new Person('tanaka', 'taro')
let person2 = new Person('yamada', 'hanako')
person1.introduce(); // My name is tanaka taro
person2.introduce(); // My name is yamada hanako
このままでは定義したintroduce関数
を書き換えることができてしまいます。
ここから分かることは、person1
とperson2
はそれぞれ別の関数を呼んでいることがわかります。
コンストラクタやクラスの概念で言うと、あるクラスのあるメソッドは同じように機能してほしいはずです。
どこからでも書き換えることができて、違う結果を生み出す関数は、バグを生みやすいですし、クラスの概念からかけ離れてしまいます。
そこで使えるのがprototype
です。
次章から見ていきましょう。
function Person(first, last) {
this.first = first;
this.last = last;
this.introduce = function() {
console.log('My name is ' + first + ' ' + last)
}
}
let person1 = new Person('tanaka', 'taro')
let person2 = new Person('yamada', 'hanako')
person1.introduce = function() {
console.log('change the word');
}
person1.introduce(); // Change the word
person2.introduce(); // My name is yamada hanako
#prototype
主に使い方としてはコンストラクタ関数で使用したい「メソッド」を格納します。
prototypeの中で関数を定義するにはthisを使います。
このthisはPersonコンストラクタ関数を参照しています。
また、person1でインスタンス化されたあとは、person1オブジェクトがthisの参照先になります。
コンストラクタ関数の場合はprototypeオブジェクトにアクセスすることで内容を変更できます。
インスタンス化した後は、__proto__
というオブジェクトに格納され直すので、__proto__.introduce
を書き直すことで同じ結果を生み出すことができます。
動作の原理を理解するために記述しましたが、__proto__
で内容を書き換えることは推奨されていないので、一度関数を定義したら本来は書き換えないのが理想です。
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.introduce = function() {
console.log('My name is ' + this.first + ' ' + this.last)
}
let person1 = new Person('tanaka', 'taro')
let person2 = new Person('yamada', 'hanako')
person1.__proto__.introduce = function() {
console.log('change the word');
}
person1.introduce(); // Change the word
person2.introduce(); // Change the word
図解にすると下図のようになります。
####フロー
- Personというコンストラクタ関数を定義します。
- first,lastというプロパティを定義します。prototypeプロパティはPersonコンストラクタ関数を定義したとき暗黙的に定義されるプロパティです。
- new演算子用いてインスタンス化し、person1オブジェクトを生成します。
- inatroduceメソッドを呼び出して、コンソールに文字列を出力します。
####メソッドチェーン
- インスタンス化されたperson2にintroduceメソッドが存在した場合には、person2.introduce()が実行されます。
- もし無ければ、
__proto__
はPerson.prototypeオブジェクトの参照を保持しているので、__proto__
を参照して、Person.prototypeオブジェクトにintroduceメソッドが無いかを探します。 - もし無ければ、Object.prototypeオブジェクトにintroduceメソッドを探しにいきます。
- 無ければ、エラーが返ってきます。
このように多層構造にプロトタイプが形成されているものをプロトタイプチェーンと呼びます。
####コンソールでおさらい
Person.prototypeオブジェクトの中にintroduce関数が格納されています。
constructorはPersonコンストラクタ関数が定義された時に合わせて暗黙的に定義されるものです。
無ければ、Object.prototypeオブジェクトの中にintroduce関数があるか探します。
あれば呼び出し、無ければエラーを返します。
#継承
継承とは、新しい機能を追加したい&既存のコンストラクタ関数の機能も使いたい、となった時に最低限の記述でまるっと機能が使えてしまう方法のことです。
まずは以下のコードを見てみましょう。
function Person(first, last) {
this.first = first;
this.last = last;
}
// 追加はじまり
function Hobby(first, last) {
this.first = first;
this.last = last;
this.myHoby = function() {
console.log('My hobby is running!')
}
// 追加おわり
Person.prototype.introduce = function() {
console.log('My name is ' + this.first + ' ' + this.last)
}
let person1 = new Person('tanaka', 'taro')
person1.introduce();
上記コードでは、新しくHobbyコンストラクタ関数を定義して、新しくmyHobbyというメソッドを追加しています。
この記述方法では、記述が重複していて凄く冗長ですよね。
修正やテストは2回やらなければいけないし、記述量が2倍になった分、バグも発生しやすくなります。
これらのコードを修正しましょう。
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.introduce = function() {
console.log('My name is ' + this.first + ' ' + this.last)
}
// 追加はじまり
// call関数を使用して、Personコンストラクタのプロパティを継承
function Hobby(first, last) {
Person.call(this, first, last);
// 新しいプロパティ追加
this.sports = 'handball'
}
// prototypeオブジェクトで定義された関数を継承
Object.setPrototypeOf(Hobby.prototype, Person.prototype)
// Person.prototypeオブジェクトで定義されたintroduce関数を上書き
Hobby.prototype.introduce = function() {
console.log('My hobby is ' + this.sports)
}
// 追加おわり
let myHobby = new Hobby('tanaka', 'taro')
myHobby.introduce(); // 「 My name is tanaka taro 」 ではなく、 「 My hobby is handball 」 が出力される
そもそもですが、Personコンストラクタ内のthisとHobbyコンストラクタ内のthisは別物になります。これらは同じにしないといけないですよね。
そこで、Hobbyコンストラクタ内で実行するthisとPersonコンストラクタ内で実行するthisは同じですよ!と明示的に示しているのがcall関数になります。
callの特徴としては、第一引数に渡したものを親コンストラクタ内で実行されるときに呼び出されたthisと同じものにすると言う意味合いがあります。この場合、thisをbindするとも言います。
第二引数以降は呼び出した親のコンストラクタの引数と同じにします。
Person.prototypeオブジェクトで定義したintroduce関数は、まだHobbyコンストラクタに継承されていません。
prototypeオブジェクトで定義した関数を継承する時には、Object.setPrototypeOfというメソッドを使用します。
使い方ですが、第一引数は継承先、第二引数は継承元となります。
Personで定義したようなintroduce関数をHobbyでも定義すると、継承先の呼び出しが優先されるのも注意しましょう。
#クラス演算子
それではお馴染みのクラスに書き換えていきましょう。
class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
introduce() {
console.log('My name is ' + this.first + ' ' + this.last)
}
}
// extendsでprototypeオブジェクトも継承されます
class Hobby extends Person {
constructor(first, last) {
// Personクラスのconstructorを呼び出します。
super()
this.sports = 'handball'
}
introduce() {
console.log('My hobby is ' + this.sports)
}
}
let person1 = new Person('tanaka', 'taro')
person1.introduce(); // My name is tanaka taro
だいぶコンパクトになりましたね。
以上