こんにちは!ライターのヨシダジュンです。
JavaScriptプログラミングにおける「コンストラクタ」をご存知でしょうか。
オブジェクト指向型のプログラミング言語にはある概念なので、馴染みのある方も多いでしょう。
ちなみに、JavaScriptはECMAScript 2015(ES6)より以前はクラス構文がありませんでした。
この記事では、
・コンストラクタとは
・メソッドを定義する方法
という、頭に入れておくとよい基本的な内容から、
・擬似的にオーバーロードする方法
・コンストラクタのreturn値について
・クラスの作成(ECMAScript 2015以降)
といった応用編に関しても解説していきます。
今回はそんな「コンストラクタ」について、わかりやすく解説します!
コンストラクタとは
JavaScriptのコンストラクタとは
コンストラクタという単語を聞くと、セットでついてくるのがクラスのイメージですが、実はJavaScriptにはクラスがありませんでした。
しかし、ECMAScript 2015(ES6)からクラスを使うことができます。
ECMAScript 2015(ES6)より以前では
「クラスがないのにコンストラクタがあるってどういうことだろう?」
と疑問を持たれた方もいらっしゃるかもしれませんが、焦らずに大丈夫です。
順番にご説明していきます。
そもそもコンストラクタとは何なのでしょう?
日本語に訳すと「構築子」になり、インスタンス(実体)を作成する関数のことをコンストラクタと呼びます。
コンストラクタによってインスタンスを作り、初期化も行います。
変数は宣言しただけではただの箱のようなものであり、値を代入した時点で初期化されます。
オブジェクト指向言語にあるクラスも、変数と同じようにインスタンスを初期化する必要があり、そのときにコンストラクタを使います。
しかし、上でECMAScript 2015(ES6)より以前はJavaScriptにはクラスがないということをご紹介しました。
実は、JavaScriptでは関数オブジェクトとコンストラクタを使うことで、疑似的なクラスを実現できる仕組みになっているのです。
コンストラクタの実装方法
JavaScriptでは、ECMAScript 2015(ES6)より以前はfunctionで疑似的なクラスを作ったり、new演算子でインスタンスを作っていました。
実際に疑似的なクラスを実装する方法をご説明します。
以下、サンプルコードです。
function Person(name, age) { this.name = name; this.age = age; }
Personというコンストラクタを定義して、その中に名前を表すnameと、年齢を表すageのプロパティを定義しています。
「this」を付けているのにも理由があります。
実際にインスタンスを作成する際は、以下のように「new Person()」でコンストラクタを呼び出して使います。
var person1 = new Person('太郎', 22); var person2 = new Person('次郎', 31); console.log( person1.name ); console.log( person1.age ); console.log( person2.name ); console.log( person2.age );
実行結果:
太郎 22 次郎 31
「new」を付けることで、生成されるインスタンスが「this」にセットされるわけです。
上記のサンプルコードでは「new Person(‘太郎’, 22)」のthisにperson1がセットされ、「new Person(‘次郎’, 31)」のthisにperson2がセットされます。
メソッドを定義する方法
次にメソッド定義の方法として、コンストラクタ内で定義する方法と、prototypeを使った方法についてご紹介いたします。
コンストラクタ内で定義する方法
まず、コンストラクタ内で定義する方法です。
var Person = function(name, age) { this.name = name; this.age = age; this.setName = function(name) { this.name = name; } this.getName = function() { return this.name; } }
この方法の場合、Personのコンストラクタを呼び出すたびにsetNameメソッドとgetNameメソッドがコピーされ、メモリ容量を消費するというデメリットがあります。
prototypeを使って定義する方法
次にprototypeを使ったメソッド定義の方法です。
prototypeというのはプロパティの一種で、最初は空のオブジェクトを「参照」しています。
「参照」というのはデータそのものではなく、メモリ上の他の場所にあるデータを指すものです。
prototypeを使って定義したメソッドは、インスタンスが生成されるたびにコピーされることはありません。
インスタンス化されたオブジェクトが持っているのは、メソッドの実体ではなく「参照」だからです。
メソッドがコピーされるわけではないので、メモリの節約になります。
以下、prototypeを使ったメソッド定義のサンプルコードです。
var Person = function(name, age) { if(!(this instanceof Person)) { return new Person(name, age); } this.name = name; this.age = age; } Person.prototype.setName = function(name) { this.name = name; } Person.prototype.getName = function() { return this.name; }
prototypeプロパティを使い、コンストラクタの外でメソッド定義を行っているのが分かります。
プロトタイプ(prototype)の使い方についてはこちらの記事で詳しく解説しているので、ぜひ確認してください。
擬似的にオーバーロードする方法
ケースによっては、引数の数が違うコンストラクタを複数用意しておきたい場合もあります。
例えば、引数が1個のコンストラクタと2個のコンストラクタの2つを使いたい場合などです。
これを1つのコンストラクタで実現したい場合はどのようにすれば良いでしょうか?
次のサンプルコードを見てください!
function Cat(name, age) { //期待した引数が入力されなかった場合に初期値を設定 if ( name === undefined ) name = '匿名'; if ( age === undefined ) age = '不明'; //指定の引数以上が入力された場合の条件分岐処理 if ( arguments.length > 2 ) alert('引数を正しく設定してください'); this.name = name; this.age = age; } //引数を1つしか設定しない場合 var cat1 = new Cat( 'かんな' ); console.log( cat1 );
実行結果:
Cat { name: "かんな", age: "不明" }
上記のサンプルコードでは、Catコンストラクタを作成し「名前」と「年齢」プロパティを設定しています。
実際にインスタンスを作成する時に、本来は2つの引数を設定するわけですが、名前しか設定しない場合に条件分岐で初期値を設定しているのが分かります。
今回のサンプルだと「年齢」が入力されていなかったので、初期値である「不明」が設定されたわけです。
これは、期待していた引数が設定されていないと「undefined」になる特性を利用しているわけですが、逆に引数が多い場合には「arguments」を使って条件分岐をすることが出来ます。
コンストラクタのreturn値について
通常のコンストラクタの使い方としては、新しいthisオブジェクトを生成してreturnで返すというものです。
コンストラクタが他のメソッドと異なる点としては、明示的にthisオブジェクトを作成したりreturnを記述する必要がないという点です。
以下のサンプルコードを見てください。
var Dog = function () { //var this = {}; の記述はいらない this.name = 'pochi'; //return this; の記述はいらない }; var myDog = new Dog(); console.log( myDog.name );
実行結果:
pochi
このサンプルでコメントアウトしている部分を見ると分かりますが、新しくthisオブジェクトを作ったり、そのthisオブジェクトをreturnで返す処理は不要というのが分かります。
しかしながら、意図的に任意のオブジェクトをreturnしてしまうと予想とは違う動作をします。
次のサンプルコードを見てください!
var Dog = function () { this.name = 'pochi'; //意図的に空のオブジェクトを返してみる return {}; }; var myDog = new Dog(); console.log( myDog.name );
実行結果:
undefined
上記の場合、空のオブジェクト「{}」を返しているのが分かります。
そのため、そのオブジェクトを使ってnameプロパティにアクセスしようとするのでundefinedエラーとなってしまうわけです。
しかし、以下の場合は異なる動作になります。
var Dog = function () { this.name = 'pochi'; //オブジェクト以外の任意の基本型を返す return 1; }; var myDog = new Dog(); console.log( myDog.name );
実行結果:
pochi
この場合、コンストラクタ内で1という数値を返していますが、undefinedエラーではなくthisオブジェクトが正しく返されてnameプロパティにアクセスできています。
ここから分かるのは、コンストラクタ内で生成した数値などの基本型は無視され、コンストラクタ内で生成した任意のオブジェクトについては返されるということですね。
コンストラクタの戻り値を正しく理解し、プログラムを作成できるようにしておきましょう!
クラスの作成(ECMAScript 2015以降)
ECMAScript 2015(ES6)からJavaScriptでもクラスが導入されました。
クラスはJavaScriptにすでにあるプロトタイプベース継承の糖衣構文です。
糖衣構文とは、プログラミング言語において読み書きのしやすさのために導入される書き方のことです。
ですので新しいオブジェクト指向継承モデルが導入されたのではなく、複雑でわかりにくい書き方と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書けるようにしています。
クラスはあるクラスにアクセスする前に、そのクラスを宣言しておく必要があります。
クラスの定義
クラスの定義の例をみてみましょう。
class Person { constructor(name, age) { this.name = name; this.age = age; } } var person = new Person('太郎', 22); console.log( person.name ); console.log( person.age );
実行結果:
太郎 22
クラスではメソッド名constructorでコンストラクタを定義します。
メソッドの定義
クラス内にメソッドを定義することもできます。
class Person { constructor(name, age) { this.name = name; this.age = age; } get result() { this.checkAge(); } checkAge() { if(this.age < 20) { console.log(this.name + 'は未成年です'); } else { console.log(this.name + 'は成人です'); } } } var person = new Person('太郎', 22); person.result;
実行結果:
太郎は成人です
クラスの継承
JavaやC++、C#などのオブジェクト指向言語のようにクラスを継承して機能を追加させることもできます。
クラスを継承するには、extends句を使います。
class Person { constructor(name, age) { this.name = name; this.age = age; } } class ChildPerson extends Person { get result() { this.checkAge(); } checkAge() { if(this.age < 20) { console.log(this.name + 'は未成年です'); } else { console.log(this.name + 'は成人です'); } } } var person = new ChildPerson('太郎', 22); person.result;
実行結果:
太郎は成人です
まとめ
今回は、JavaScriptにおけるコンストラクタについてご紹介しました。
最後にもう一度ポイントをおさらいしておきましょう。
・コンストラクタはfunctionで作成し、newでインスタンスを生成する。
・コンストラクタのメソッド定義は、prototypeを使うことが望ましい。
・引数の数が異なるコンストラクタを疑似的にオーバーロードすることができる。
・コンストラクタは明示的にreturnしなくとも、thisオブジェクトを返す。
・ECMAScript 2015(ES6)からクラスが導入され、よりシンプルでわかりやすい書き方で書ける。
これらの内容を理解した上で、ぜひ自分のプログラムにも応用できるように挑戦してみましょう。