JavaScriptのConstructor関数の基本的な使い方

2021年3月7日JavaScript

JavaScriptのConstructor関数の基本的な使い方を紹介します。

Constructor関数とインスタンス

Constructor(コンストラクター)関数とは、新しくオブジェクトを作成するための雛形となる関数です。
呼び出し方は、一般の関数と異なり、専用のnew演算子を使います。
new演算子をつけて呼ばれた関数は、オブジェクトを生成します。
このnew演算子を使ってコンストラクター関数からオブジェクトを生成することをインスタンス化といいます。
生成されたオブジェクトをインスタンスといいます。

Constructor関数の使い方

Constructor関数は、関数宣言か関数式で書く(アロー関数は✗)

Constructor関数は、関数宣言の形でも関数式の形でも使えます。
しかしアロー関数では使えません
アロー関数にはthisキーワードがなく、Constructor関数にはthisキーワードが必要だからです。

new演算子

Constructor関数からインスタンスを生成するには、new演算子をつけます。
new演算子は、Constructor関数からインスタンスを作成するために使用する演算子です。

{
  const Person = function(firstName, birthYear) {
    console.log(this);//Person {}
  }
  
  const sasaki = new Person('sasaki', 1988);
}

new演算子は特別な演算子です。Personを呼び出すだけでなく、次のことを行います。

  1. 空のオブジェクトがつくられる
  2. thisキーワードが空のオブジェクトにセットされる
  3. 空のオブジェクトがprototypeにリンクされる
  4. 関数から自動的に空のオブジェクトをreturnされる
  5. 空のオブジェクトが変数に格納される

実際に、thisをコンソールに表示すると「Person {}」が表示されます。
new演算子の挙動はConstructor関数のreturnがオブジェクトとそれ以外の場合によって変化するので、確認してみましょう。

new演算子の挙動

Constructor関数の戻り値がオブジェクトの場合

Constructor関数の戻り値がオブジェクトの場合、new演算子によって、Constructor関数のreturnのオブジェクト{…}を新しいインスタンスオブジェクトとして呼び出し元に返却します。

function Fn(a, b) {
  this.a = a;
  this.b = b;
  return {};
}

//prototypeでメソッドを追加
Fn.prototype.aaa = function () {};

const instance = new Fn(3, 4);
console.log(instance); //{}

returnがオブジェクトのとき、そのオブジェクトが返ってきます。
またprototypeでメソッドを追加しても反映されません。

Constructor関数の戻り値がオブジェクト以外の場合

Constructor関数の戻り値がオブジェクト以外の場合、
インスタンスオブジェクトを作成して、Constructor関数のprototypeの値を__proto__にコピーしてインスタンスオブジェクトをthisを呼び出し元に返却します。

//戻り値がオブジェクト以外のとき
function Fn2(a, b) {
  this.a = a;
  this.b = b;
  return 5;
}

//prototypeでメソッドを追加
Fn2.prototype.aaa = function () {};

const instance2 = new Fn2(3, 4);
console.log(instance2); //Fn2 {a: 3, b: 4}

戻り値がオブジェクト以外(今回は例としてプリミティブ型)のとき、this(Fn2)に格納されたaとbをもったオブジェクトが生成されます。
また、prototypeで追加したメソッドも存在します。
戻り値が定義されていないときも同様の結果となります。

Constructor関数の戻り値が定義されていない場合

//戻り値が定義されていないとき
function Fn3(a, b) {
  this.a = a;
  this.b = b;
  //returnなし
}

//prototypeでメソッドを追加
Fn3.prototype.aaa = function () {};

const instance3 = new Fn3(3, 4);
console.log(instance3); //Fn2 {a: 3, b: 4}

戻り値が定義されていないときも同様で、this(Fn3)に格納されたaとbをもったオブジェクトが生成されます。
また、prototypeで追加したメソッドも存在します。

Constructor関数の具体例


//コンストラクタ関数を作成
const Person = function(firstName, birthYear) {
  this.firstName = firstName;
  this.birthYear = birthYear;
}

//インスタンスを作成
const yamada = new Person('Yamada', 1988);
console.log(yamada);//Person {firstName: "Yamada", birthYear: 1988}

  1. New {} という空のオブジェクトがつくられます。
  2. そして関数が呼び出されます。そのときにthisキーワードは上の空のPerson {}にセットされます。そして、このときthisキーワードの後にプロパティとプロパティと同じ名前のものを紐付けています。プロパティ名は自由ですが、Personの引数とプロパティの値は同じである必要があります。
  3. {}がprototypeにリンクされます。
  4. 関数から自動的に{}をreturnされて、それが変数yamadaに格納されます。

これで、他にもインスタンスを追加してみます。


const sasaki = new Person('Sasaki', 2020);
const kondo = new Person('Kondo', 2020);
console.log(sasaki, kondo);
//Person {firstName: "Sasaki", birthYear: 2020}
//Person {firstName: "Kondo", birthYear: 2020}

yamada、sasaki、kondoはPersonのインスタンスということができます。
このように、とても簡単にインスタンスをつくることができます。
また、インスタンスかどうかを確認する方法もあります。

instanceofでインスタンスか確認する

instanceofは、どのコンストラクタから生成されたオブジェクトかを確認します。
つまり、どのConstructor関数のインスタンスか確認できます。


console.log(yamada instanceof Person);//true

このように、true/falseでどのインスタンスかを確認することができます。

 

Constructor関数で子クラスを作る方法

まずは、重複したコードがありますが、わかりやすいので次のコードをみてみましょう。


// クラスとConsructor関数
{
  const PersonConstructor = function (lastName, birthYear) {
    this.lastName = lastName;
    this.birthYear = birthYear;
  };
  PersonConstructor.prototype.calcAge = function () {
    console.log(2050 - birthYear);
  };

  const Student = function (lastName, birthYear, course) {
    this.lastName = lastName;
    this.birthYear = birthYear;
    this.course = course;
  };

  Student.prototype.introduce = function () {
    console.log(`私は${this.lastName}です。${this.course}を勉強しています。`);
  };

  const kobayashi = new Student('小林', 2030, '数学');
  console.log(kobayashi); //Student {lastName: "小林", birthYear: 2030, course: "数学"}

  kobayashi.introduce(); //私は小林です。数学を勉強しています。
}

  console.log(kobayashi instanceof Student); //true
  console.log(kobayashi instanceof PersonConstructor); //false

上の例では、正常に動作していますが、コードが重複している部分があります。
これでは、PersonConstructorが変更されたとき、Studentも同じ変更をしないといけません。
そして、この場合、kobayashiというリアルデータを保持したオブジェクトはStudentのインスタンスであって、PersonConstructorのインスタンスではない状態となります。
理想は、Personという人であり、Studentという生徒であってほしいので、これでは正しくありません。
そこで、つぎのように書き換えます。

 


{
  //親クラス
  const PersonConstructor = function (lastName, birthYear) {
    this.lastName = lastName;
    this.birthYear = birthYear;
  };
  PersonConstructor.prototype.calcAge = function () {
    console.log(2050 - this.birthYear);
  };

  //子クラス
  const Student = function (lastName, birthYear, course) {
    // PersonConstructor(lastName,birthYear) //これはできない。
    // thisキーワードを親クラスに変更する
    PersonConstructor.call(this, lastName, birthYear);
    this.course = course;
  };

  //prototype = prototypeOfLinkedObjectと解釈する

  Student.prototype = Object.create(PersonConstructor.prototype); //Student.prototypeがPersonConstructor {}になる。
  // Student.prototype = PersonConstructor.prototype;//これはやりたいことと異なる

  Student.prototype.introduce = function () {
    console.log(`私は${this.lastName}です。${this.course}を勉強しています。`);
  };

  const kobayashi = new Student('小林', 2030, '数学');
  console.log(kobayashi); //Student {lastName: "小林", birthYear: 2030, course: "数学"}

  kobayashi.introduce(); //私は小林です。数学を勉強しています。
  kobayashi.calcAge(); //20
  console.log(kobayashi instanceof Student); //true
  console.log(kobayashi instanceof PersonConstructor); //true
}

thisキーワードの指す先を変更する


    // PersonConstructor(lastName,birthYear) //これはできない。
    // thisキーワードを親クラスに変更する
    PersonConstructor.call(this, lastName, birthYear);

重複コードをPersonConstructorから呼び出せばいいと、「PersonConstructor(lastName,birthYear)」としても動きません。
これは通常の関数の呼び出しになり、thisキーワードはundefinedとなります。
PersonConstructor.call(this, lastName, birthYear)とcallメソッドでthisキーワードを手動で設定すれば正常に動作します。

子クラス.prototype = Object.create(親クラス.prototype)

この段階では、親クラスも子クラスも別々のコンストラクタ関数です。
それをつまりどちらのプロトタイプもObjectになっています。
やりたいことは、親クラスのリンクされたオブジェクトを子クラスのリンクされたオブジェクトとして使いたいところです。
そのため、次のようにプロトタイプをリンクさせます。
Student.prototype(Studentのリンクされたオブジェクトのプロトタイプ)にPersonConstructorのリンクされたオブジェクトのプロトタイプを代入します

  Student.prototype = Object.create(PersonConstructor.prototype);
 //Student.prototypeがPersonConstructor {}になる。

子クラス.prototype = 親クラス.prototypeではない

  Student.prototype = PersonConstructor.prototype;
//これはやりたいことと異なる

「Student.prototype = PersonConstructor.prototype」はやりたいことと異なる結果になります。
このprototypeは、「リンクされたオブジェクトのプロトタイプ」という意味です。
つまり、kobayashiというオブジェクトもStudentというコンストラクタ関数のどちらも直接PersonConstructorのプロトタイプを参照するようになってしまい、Studentが子クラスとして機能できません。

コンストラクタ関数で親クラス>子クラス>オブジェクトのリンクのさせ方

kobayashiというリアルなデータを入れるオブジェクトと子クラスであるStudentにひもづけるには、new演算子を使います。
そして、子クラスと親クラスをひもづけるには、手動でプロトタイプを操作できるObject.create()で、親クラスの「リンクされたオブジェクトのプロトタイプ===PersonConstructor.prototype」を子クラスの「リンクされたオブジェクトのプロトタイプ===Student.prototype」に代入します。
子クラスのthisキーワードも同じく手動でthisキーワードを操作できるcall()メソッドを使って設定します。