In 'n' Out

知識を取り込み、そして発信する

コンストラクター

コンストラクターとインスタンス

JavaScriptでは、同じ構造を持つオブジェクトを複数扱う場面がよくあります。
例えばユーザー情報や商品データのように、同じ項目を持つデータを何度も作る場合です。
そのたびにオブジェクトを個別に書いていると、記述が増え管理もしづらくなります。
このようなときに使うのがコンストラクターです。
コンストラクターは、同じ形のオブジェクトをまとめて作るための仕組みです。
そして、コンストラクターを使って実際に作られたオブジェクトをインスタンスといいます。
つまり、コンストラクターが作るための仕組みであり、インスタンスは作られた結果です。
同じコンストラクターから複数のデータを作った場合でも、それぞれは独立したインスタンスとして扱われます。
それぞれが別の値を持ち、個別に操作できるのが特徴です。
コンストラクターとインスタンスの関係がわかるよう、たとえ話をしてみます。
A・B・Cさんに同じ内容のメールを送るとします。
しかし、宛名はそれぞれ変更する必要があります。
このとき、ひな型となる文面がコンストラクターにあたります。
そして、そのひな型をもとにA・B・Cさんそれぞれに送られたメールがインスタンスです。
また、勘違いしがちですが、インスタンスは単なる複製ではありません。

構文

function コンストラクター名(引数){
    this.プロパティ名 = 値;
};

const 変数 = new コンストラクター名(値);

コンストラクター関数は、同じ構造を持ったオブジェクトを作るためのものです。
thisは生成されるオブジェクト自身を指し、プロパティを設定するために使われます。
コンストラクター関数では、thisに設定した内容が新しいオブジェクトのプロパティとして追加されます。
newを付けて実行することで、コンストラクターから新しいオブジェクトが作成されます。
このとき作られたオブジェクトをインスタンスといいます。
つまり、コンストラクターはオブジェクトを作るための仕組みであり、インスタンスはその結果として生成された実体です。
理解を深めるため、通常の関数とコンストラクター関数を比較してみます。

通常の関数コード例

//functionNormal1.js
function item(name, price){
    return{
       name: name,
       price: price
    };
}
       
const items1 = item('りんご', 150);
const items2 = item('みかん', 300);
alert(items1.name + '\n' + items2.name);

動作例

※このボタンは動作確認用です

コンストラクター関数コード例

//constructor1.js
function item(name, price){
    this.name = name;
    this.price = price;
}

const items1 = new item('りんご', 150);
const items2 = new item('みかん', 300);
alert(items1.name + '\n' + items2.name);

動作例

※このボタンは動作確認用です

この2つは見た目の結果は同じになります。
しかし、ここまでは特に恩恵を受けた感覚はないと思います。
では、項目が追加され、そのデータを使う場合を見比べてみましょう。

通常の関数コード例

//functionNormal2.js
function item(name, price){
    return{
       name: name,
       price: price
    };
}
       
const items1 = item('りんご', 150);
const items2 = item('みかん', 300);

items1.stock = 10;
items1.category = '果物';

items2.stock = 5;
items2.category = '果物';

alert(items1.stock + '\n' + items2.stock);

動作例

※このボタンは動作確認用です

コンストラクター関数コード例

//constructor2.js
function item(name, price, stock, category){
    this.name = name;
    this.price = price;
    this.stock = stock;
    this.category = category;
}

const items1 = new item('りんご', 150, 10, '果物');
const items2 = new item('みかん', 300, 5, '果物');
alert(items1.stock + '\n' + items2.stock);

動作例

※このボタンは動作確認用です

途中で扱うプロパティが増えてしまった場合に最初に定義した内容とは別に、あとからプロパティを追加する必要が出てきます。
コンストラクター関数を使うことで、最初から必要なプロパティをまとめて定義できるため、後から追加する必要がなくなり、データの構造を統一することができます。
さらにはプロパティの可読性も上がっていることがわかると思います。
もちろん、通常の関数でも修正は可能ですが、どこでプロパティが追加されたのか分かりづらくなります。
コンストラクター関数であれば、定義が1か所にまとまっているため、問題が発生した場合でも原因を特定しやすくなります。
なお、プロパティ名は必ず引数と同じにする必要はありませんが、プロパティには引数として受け取った値を代入して設定します。

プロトタイプ

コンストラクター関数を使うことで、インスタンスを作成できると説明しました。 しかし、インスタンスに関数(メソッド)を持たせる場合、その書き方によっては同じ処理が何度も作られてしまうという問題があります。
例えば、コンストラクター内で関数を定義すると、インスタンスを生成するたびにその関数も新しく作られます。
これは動作としては問題ありませんが、同じ内容の関数が複数存在することになり、無駄が生じてしまいます。
このようなときに使うのがプロトタイプです。
プロトタイプを使うことで、複数のインスタンスで共通して使う処理を1つにまとめることができます。
つまり、インスタンスごとに持たせたいデータはコンストラクター(this)に、共通で使う処理はプロトタイプに定義することで、効率的で見通しの良いコードを書くことができるようになります。

構文

コンストラクター名.prototype.メソッド名 = function(){
    処理;
};

prototypeは、コンストラクター関数で作られたインスタンスに対して使われます。
コンストラクターでデータセットしたインスタンスにprototype.メソッド名にすることにより、共通処理を書くことができます。
その共通処理を関数化することでプロトタイプは効果を発揮します。

コード例

//prototype1.js
function item(name, price, stock) {
    this.productName = name;
    this.productPrice = price;
    this.productStock = stock;
}

item.prototype.sum = function () {
    alert(this.productName + 'は全合計で' + this.productPrice * this.productStock + '円です');
}

const item1 = new item('りんご', 150, 10).sum();
const item2 = new item('みかん', 300, 5).sum();
const item3 = new item('いちご', 700, 12).sum();

動作例

※このボタンは動作確認用です

まず、コンストラクター関数を作成し、prototypeを使って共通処理をsumとして定義します。
new演算子でインスタンスを作成し、各itemでsum関数を呼び出すことで結果を表示できます。
配列を使わなくとも、それぞれのインスタンスで処理を実行することは可能です。
このコードは動作としては問題ありませんが、設計上の問題があります。
それは、計算処理と表示処理を1つの関数にまとめてしまっている点です。
このようにしてしまうと、計算結果だけを別の用途で使いたい場合や、表示方法を変更したい場合に対応しづらくなります。
例えばalertではなくconsoleに表示したい場合でも、関数の中身を書き換える必要が出てしまいます。
では処理を分離したコード例に書き換えます。

コード例

//prototype1.js
function item(name, price, stock) {
    this.productName = name;
    this.productPrice = price;
    this.productStock = stock;
}

item.prototype.sum = function () {
    return this.productPrice * this.productStock;
}

item.prototype.viewAlert = function () {
    alert(this.productName + 'は全合計で' + this.sum() + '円です');
}

const item1 = new item('りんご', 150, 10).viewAlert();
const item2 = new item('みかん', 300, 5).viewAlert();
const item3 = new item('いちご', 700, 12).viewAlert();

動作例

※このボタンは動作確認用です

それぞれの役割を考えてprototypeで分割することは重要ですが、あまりにも細かく分けすぎると、かえって使いづらくなることもあります。
そのため、まとめて処理するべきものと、分けておくべきものを意識して設計することが大切です。
計算するsum関数とalertを表示するviewAlertに分離しました。
分離したことでsum関数はその値を使う必要があるため、returnで戻り値として設定する必要があります。
それをviewAlertに渡して表示する仕組みとなります。
しかしこのままでは、インスタンスを作成するたびにviewAlertまで呼び出す必要があります。
そこで配列を使ったコードに書き換えます。

コード例

//prototype1.js
function item(name, price, stock) {
    this.productName = name;
    this.productPrice = price;
    this.productStock = stock;
}

item.prototype.sum = function () {
    return this.productPrice * this.productStock;
}

item.prototype.viewAlert = function () {
    alert(this.productName + 'は全合計で' + this.sum() + '円です');
}

const itemArray = [
    new item('りんご', 150, 10),
    new item('みかん', 300, 5),
    new item('いちご', 700, 12),
    new item('メロン', 1500, 2)
];

itemArray.forEach(function (rep){
    rep.viewAlert();
});

動作例

※このボタンは動作確認用です

配列にインスタンスしたオブジェクトを格納し、forEachを使いviewAlertで表示するようにしました。
それぞれの役割ごとに設定することで見通しが良くなります。
また、2次元以上の多次元配列になると、データの意味合いが理解しやすくなります。
そのため、最初にalertまで含めたコンストラクター関数を使うと配列は不要になると思ったのではないでしょうか。
しかし、そもそもコンストラクター関数はデータの形を作るもの、配列はデータをまとめて扱うものです。
役割が異なるため、どちらか一方ではなく、組み合わせて使うことが重要です。
配列と使い分けることで意味のあるコードを意識して記述できるようになります。

JavaScriptに用意されているコンストラクター関数

これまで、コンストラクター関数について見てきましたが、JavaScriptには自分で作成するコンストラクター関数だけでなく、あらかじめ用意されているコンストラクター関数が存在します。
これらはすべてnewを使ってインスタンスを作成できる関数です。
下記はそのコンストラクター関数の一覧になります。

コンストラクター関数一覧
コンストラクター関数判別する型インスタンス方法
Objectオブジェクトnew Object()
Array配列new Array()
String文字列オブジェクトnew String()
Number数値オブジェクトnew Number()
Boolean真偽値オブジェクトnew Boolean()
BigIntBigIntBigInt()
SymbolSymbolSymbol()
Function関数new Function()
Date日付new Date()
RegExp正規表現new RegExp()
Errorエラーオブジェクトnew Error()
EvalErrorエラーnew EvalError()
RangeErrorエラーnew RangeError()
ReferenceErrorエラーnew ReferenceError()
SyntaxErrorエラーnew SyntaxError()
TypeErrorエラーnew TypeError()
URIErrorエラーnew URIError()
JSONJSONオブジェクトインスタンス不可
Math数学オブジェクトインスタンス不可
Mapマップnew Map()
Setセットnew Set()
WeakMap弱参照マップnew WeakMap()
WeakSet弱参照セットnew WeakSet()
Promise非同期処理new Promise()
Proxyプロキシnew Proxy()
Reflectリフレクトインスタンス不可
ArrayBufferバイナリデータnew ArrayBuffer()
SharedArrayBuffer共有バイナリデータnew SharedArrayBuffer()
DataViewデータビューnew DataView()
Int8ArrayTypedArraynew Int8Array()
Uint8ArrayTypedArraynew Uint8Array()
Uint8ClampedArrayTypedArraynew Uint8ClampedArray()
Int16ArrayTypedArraynew Int16Array()
Uint16ArrayTypedArraynew Uint16Array()
Int32ArrayTypedArraynew Int32Array()
Uint32ArrayTypedArraynew Uint32Array()
Float32ArrayTypedArraynew Float32Array()
Float64ArrayTypedArraynew Float64Array()
BigInt64ArrayTypedArraynew BigInt64Array()
BigUint64ArrayTypedArraynew BigUint64Array()

String・Number・Booleanは通常はプリミティブ値として扱われますが、newを使うとオブジェクトとして生成されます。

これらはすべて覚える必要はありませんが、次ページで扱うinstanceofで使用するため、Object・Array・String・Number・Boolean・Date・RegExpあたりは目を通しておきましょう。
なお、これらは実際にはコンストラクター関数の構文で生成されていますが、その内部の処理を確認することはできません。
これは、JavaScriptで書かれているのではなく、エンジン内部(ネイティブコード)で実装されているためです。
そのため、組み込みのコンストラクター関数は内容を直接確認できず、開発者が変更することもできません。