オブジェクト構文関数・メソッド

【JavaScript】 コンストラクターとは?関数とは違うのか?

更新日:2020/02/29

JavaScriptにはコンストラクター関数というものがあります。
ネット等で調べると、コンストラクターの使い方の説明はされていますが、普通の関数との違いについては何も言及されていないことが多いです。

そこで今回は、コンストラクター関数について詳しくお伝えします。

 

コンストラクター関数とは

コンストラクター関数とは、new式を使用して新規オブジェクト(インスタンス)を作成する関数です。

次のスクリプトコードは、コンストラクター関数の定義と使用法の一番簡単な例です

コンストラクター関数の定義と使用例


function a(x){
    this.x = x;
    return this;
}
const b = new a(1);

このコードでは、次のことをおこなっています。

  • 関数a内で、オブジェクトthisのxプロパティに、値をセットしている。
  • オブジェクトthisをリターンしている。
  • new式で、関数aを呼び出している

このとき関数aは、コンストラクター関数と呼ばれます。

このコードで何が起こっているのか、順番に見ていきます。

  1. コンストラクター関数をnew式で呼び出すと、コンストラクター関数のprototypeプロパティを継承したthisという名前の新規オブジェクトが作成される。

    これは、JavaScriptの内部処理でおこなわれます。

    thisという名前の新規オブジェクト

    参考記事:
    【JavaScript】 new演算子は何をやっている?

  2. 関数内コードで、thisに新規のプロパティを追加する。

    これはオブジェクトを初期化するという意味があります。

    オブジェクトを初期化

  3. 関数からリターンされたthisが、new式の結果となります。

    リターンされたthisが、new式の結果

  4. ここで、コンストラクター関数aから新規オブジェクトを受け取った変数bは、コンストラクターaのインスタンスと呼ばれることがあります。
    (実際には関数内で作成された新規オブジェクトそのものがインスタンス)

    なお、コンストラクター関数で return を使用しないと、暗黙的にthisが返ります。
    そのため、最初のコードは次のように記述できます。

    コンストラクター関数は暗黙的にthisを返す

    
    function a(x){
        this.x = x;
    }
    const b = new a(1);
    
    

 

コンストラクター関数と普通の関数の違い

ここまでコンストラクター関数の概略について解説しました。

しかし、こんな疑問を感じる人もいるはずです。

普通に関数宣言しているだけですよね?

解説者:はい、そうです。

コンストラクターなのでは?

解説者:コンストラクターです。

???

最初の例のコンストラクター関数部分だけをもう一度記載します。

コンストラクター関数の定義


function a(x){
    this.x = x;
    return this;
}

普通にfunction式を使用して、関数を定義しているだけです。
ですが、これはコンストラクター関数です。
普通の関数と、どこが異なるのでしょうか。

JavaScriptの言語仕様書には、次のように書いてあります。

summarizes additional essential internal methods that are supported by objects that may be called as functions. A function object is an object that supports the [[Call]] internal method. A constructor is an object that supports the [[Construct]] internal method. Every object that supports [[Construct]] must support [[Call]]; that is, every constructor must be a function object. Therefore, a constructor may also be referred to as a constructor function or constructor function object.

出典:6.1.7.2Object Internal Methods and Internal Slots | ECMAScript® 2019 Language Specification

赤文字を訳すと、次のようになります。

「コンストラクターは、[[Construct]]内部メソッドをサポートするオブジェクトです。」
「すべてのコンストラクターは関数オブジェクトでなければなりません。」

つまり、[[Construct]]という隠しメソッドを持つオブジェクトがコンストラクターです。

また、function文で定義されている関数は、コードの読み込み時に関数オブジェクトに変換されます。
その際、[[Construct]]メソッドもセットされます。

つまり、「関数定義 = コンストラクター関数定義」と言えるのです。

関数オブジェクトは、作成方法によっては[[Construct]]メソッドがセットされません。
したがって、「関数オブジェクト = コンストラクター」ではありません。

「コンストラクター関数と普通の関数の違いは何?」

という質問の答えは、

「仕様上の違いはない」

ということになります。

ただしプログラム設計上、「prototypeプロパティを継承した新規オブジェクトを作成し初期化する」ことを目的とした関数を定義した場合、この関数をコンストラクター関数と呼びます。

つまり、関数の目的によって、プログラムをする側が使い分けているということですね。

 

同時にコンストラクター関数と普通の関数の機能を持たせる

JavaScriptの組み込み関数には、コンストラクター関数と普通の関数の両方の機能を持っているものがあります。

例えば String関数です。

String関数は、引数を文字列に変換する関数です。

String関数を関数として使用


const b = String(123);
// b: "123" ← 文字列

ただし、String関数はコンストラクター関数と機能するように設計されています。
new式を使用して呼び出すと、String関数(オブジェクト)のprototypeを継承した、Stringオブジェクトのインスタンスが作成されます。

String関数をコンストラクター関数として使用


const b = new String(123);
// b: String{ "123" } ; ← オブジェクト

このように、関数は二つの機能を使い分けることができます。

もちろん、スクリプトコード上で同じような機能を実装することも可能です。

newと関数呼び出しの判断方法


function a(){

    if( new.target ){

          // new式が使用されたときの処理

    } else {

        // 関数として呼ばれたときの処理

    }
}

new式で関数が呼び出されると、new.targetに関数がセットされます。
関数として呼び出されたときは、undefinedです。

new.targetは、newというオブジェクトではありません。
ひとつの変数として考えるべきです。

なお、古いブラウザではnew.targetをサポートしていません。
この場合、thisがprototypeの継承であることを利用して、次のように判定します。

newと関数呼び出しの判断方法


function a(){

    if( this instanceof a ){

          // new式が使用されたときの処理

    } else {

        // 関数として呼ばれたときの処理

    }
}

instanceof は、オブジェクトのプロトタイプチェーンに、関数のprototypeプロパティが含まれているかどうかを判定します。
プロトタイプとprototypeの違いは、次のページを見てください。
【JavaScript】 プロトタイプとは?prototypeプロパティはプロトタイプではない件について

 

newしたオブジェクトはコンストラクターではない

newで作成したオブジェクトに対して、再度newを使用することはできません。

次のコードを実行してみます。

newしたオブジェクトをnewしてみる


function a( ){ this.x = 5; };
const b = new a( );
const c = new b( ); // TypeError: b is not a constructor

エラーになりました。

これは、new a( )で作成されたオブジェクトがコンストラクター関数ではないからです。

コンストラクター関数は、少なくとも関数である必要があります。
しかし、new式で内部的に用意されたthisオブジェクトは、関数ではありません。
結果として変数bは、関数でないthisオブジェクトを受け取ることになります。
そして、この変数bに対してnew式を使用すると、エラーになります。

僕は今まで、関数オブジェクトをnewしているのだから、関数オブジェクトが新しく作成されていると思っていました。
どうやら違うようです。

ただし次のようにすると、newで作成したオブジェクトに対して、再度newを使用することができます。

newで作成したオブジェクトに対して、再度newを使用する


function a(){
        return function (){};
}

const b = new a();
const c = new b();

コンストラクター関数aは最後に return があるので、thisではなく、関数を返します。
変数bは、関数なのでnew式を使用してもエラーになりません。

そもそもコンストラクター関数として意味を失っている(new式が必要ない)ので、実用的ではないですが、どうしてこのような結果になるのかを考えてみると、コンストラクターとnew式について理解が深まるのではないかと思います。

 

prototype.constructorとは?

関数オブジェクトには、prototype.constructorというプロパティがあります。

このプロパティは、プログラマーが直接操作ができます。

そのため、これがコンストラクター関数の本体だと思ってしまいますね。
僕も思っていました。

そして次のようなコードを作成してみたわけです。

コンストラクターを変更したい


function a(){ this.x = 5;};
a.prototype.constructor = function(){ this.x = 6;}; // コンストラクターを変更したい!!
const b = new a();
console.log( b.x ); // b.x : 5  ←変更されていない!!

prototype.constructorを変更して、newで呼び出される関数を変更したい。というのが上のコードの意図です。

変更した結果、newをすると、this.x が6になるはずです。

しかし元の関数呼び出された結果、this.x は5のままでした。

a.prototype.constructorの変更は、無効なのでしょうか?

前の項で説明したとおり、コンストラクタの本体は[[Construct]]です。
prototype.constructorには、初期値として元となる関数への参照がセットされます。

new式で呼び出されるのは、あくまで[[Construct]]です。
prototype.constructorは、関係ありません。

使い道も、あまりありません。
強いてあげるなら、次のようなものがあります。


function a(){
    this.x=5;
}
const b = new a();
const c = new b.constructor();

new式で作成したインスタンスbを使用して、同じオブジェクトインスタンスを新規作成しています。
prototype.constructorは元となるコンストラクター関数への参照なので、new式を適用できるわけです。

応用例として、不特定のオブジェクトインスタンスから、新規インスタンスを作成する、次のような関数が考えられます。


function CreateNewInstance( obj ){

    return obj.constructor ? new obj.constructor() : null;

}

ちなみに、obj.prototype.constructor() ではなくて、obj.constructor() なのは、new式でコンストラクター関数のprototypeプロパティが、プロトタイプチェーンに組み込まれるからです。

 

まとめ

コンストラクター関数とは、new式を使用して新規オブジェクト(インスタンス)を作成する関数です。

コンストラクター関数と普通の関数の違いは、プログラマが関数をコンストラクターとして使用するか、関数として使用するかどうかです。

同じものに対して別の呼び名があると、初心者は混乱します。

しっかりと解説していきたいですが、難しいですね。

更新日:2020/02/29

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。