本記事はTim Severienが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者の皆さんに感謝します。
JavaScriptオブジェクトの作成は難しいテーマです。JavaScriptはオブジェクトの作成方法が非常に多いため、初心者にとっても熟練者にとってもどの方法を使えば良いか分かりにくいのです。しかし、オブジェクト作成方法の選択肢が多く、それぞれのシンタックスがいかに違って見えても、思っているより似通っています。記事ではオブジェクトのさまざまな作成方法や、各々の選択肢がどれほどほかの方法を応用しているのかを紹介します。
オブジェクトリテラル
オブジェクトをもっとも簡単に作成できる方法、オブジェクトリテラルを紹介します。JavaScriptはオブジェクトを何もないところから作成できるとうたっています。クラスもテンプレートもプロトタイプもないところから「ポッ」とメソッドとデータを持つオブジェクトが作成できるのです。
var o = {
x: 42,
y: 3.14,
f: function() {},
g: function() {}
};
しかし、デメリットもあります。別の場所に同じ型のオブジェクトを作成する場合、オブジェクトのメソッド、データをコピー&ペーストして初期化しなければなりません。1つのオブジェクトを作成するだけでなく、まとまったオブジェクトを作成する方法が必要になります。
ファクトリー関数
ファクトリー関数を紹介します。同じ構造、インターフェイス、実装方法を共有するひとまとまりのオブジェクトを作成するのに、もっとも簡単な方法です。直接オブジェクトリテラルを作成するのではなく、関数を使ってオブジェクトリテラルを返します。複数回または複数の場所に同じ型のオブジェクトを作成する必要がある場合でも、関数を呼び出すだけで良いのです。
function thing() {
return {
x: 42,
y: 3.14,
f: function() {},
g: function() {}
};
}
var o = thing();
しかし、ファクトリー関数にもデメリットがあります。1つ1つのオブジェクトが各機能の個別コピーを含むため、メモリーの膨張が起こる可能性があるところです。オブジェクト1つに対し、その関数のコピーは1つに絞られるべきです。
プロトタイプチェーン
JavaScriptはプロトタイプチェーンという、オブジェクト間でデータを共有する組み込み手法を提供しています。オブジェクトのプロパティにアクセスすると、そのリクエストの処理をほかのオブジェクトに任せてリクエストを実行します。この手法を利用してファクトリー関数を変更し、作成する1つ1つのオブジェクトに特有のデータを含むようにし、ほかのすべてのプロパティリクエストを別の共有オブジェクトに任せます。
var thingPrototype = {
f: function() {},
g: function() {}
};
function thing() {
var o = Object.create(thingPrototype);
o.x = 42;
o.y = 3.14;
return o;
}
var o = thing();
実際、プロトタイプチェーンはよく用いられるため、JavaScriptにはサポート機能が備わっています。独自の共有オブジェクト(プロトタイプオブジェクト)を作成する必要はなく、ほかのすべての関数とともにプロトタイプオブジェクトが自動的に作成され、共有データを入れられます。
thing.prototype.f = function() {};
thing.prototype.g = function() {};
function thing() {
var o = Object.create(thing.prototype);
o.x = 42;
o.y = 3.14;
return o;
}
var o = thing();
しかし、プロトタイプチェーンにもデメリットがあります。同じ作業の繰り返しが発生するのです。プロトタイプファクトリー関数へ作業を任せると、「なんらかの」関数の先頭行と最終行が逐語的にしばしば繰り返されるのです。
ES5クラス
独自の関数を適用すれば行の繰り返しは避けられます。ES5クラスの関数を利用すると、任意の関数のプロトタイプに作業を任せるオブジェクトを作成することになり、新たに作成したオブジェクトを引数として使って関数を呼び出します。そして最後にオブジェクトを返します。
function create(fn) {
var o = Object.create(fn.prototype);
fn.call(o);
return o;
}
// ...
Thing.prototype.f = function() {};
Thing.prototype.g = function() {};
function Thing() {
this.x = 42;
this.y = 3.14;
}
var o = create(Thing);
実際には、ES5クラスもよく用いられるため、JavaScriptはサポート機能も備えています。この「create」関数は、実際は「new」というキーワードの基本的なものなので、「create」を「new」と入れ替えてもOKです。
Thing.prototype.f = function() {};
Thing.prototype.g = function() {};
function Thing() {
this.x = 42;
this.y = 3.14;
}
var o = new Thing();
ここまで、いわゆるES5クラスについて説明しました。ES5クラスは、共有データをプロトタイプオブジェクトに一任し、繰り返しのロジックを処理する「new」というキーワードに依存したオブジェクト作成関数でした。
しかし、ES5クラスにもデメリットがあります。冗長で見栄えが悪いのです。継承を実装するとさらに助長されてしまいます。
ES6クラス
ES6クラスは比較的最近JavaScriptに追加されたもので、同じことをする場合、かなりきれいなシンタックスになります。
class Thing {
constructor() {
this.x = 42;
this.y = 3.14;
}
f() {}
g() {}
}
var o = new Thing();
比較
長年にわたってJavaScript利用者はプロトタイプチェーンと付かず離れずでやってきました。現在、よく目にする一般的な手法は2つあります。1つは、クラスシンタックスでプロトタイプチェーンに大きく依存する方法。もう1つは、ファクトリー関数シンタックスで、たいていの場合プロトタイプチェーンには一切依存しない方法です。この2つの手法は異なっていますが、パフォーマンスや特徴がわずかに異なっているだけです。
パフォーマンス
JavaScriptエンジンはかなり最適化されているため、コードを見てもっと処理速度が速くなる方法を考える余地はほとんどありません。計測は重要ですが、計測しても失敗するときすらあります。JavaScriptエンジンの更新は6か月ごとにリリースされ、パフォーマンスに大きな変化があるときともあります。過去の判断材料や、その判断材料に基づいた決断が役に立たないのです。そのため、私はたいていもっとも正式で広く使われているシンタックスを好んで利用しています。この方法が多くの場合、もっともよく吟味され、もっともパフォーマンスが良いと思うからです。それがクラスシンタックスです。現時点で、クラスシンタックスはリテラルを返すファクトリー関数のおよそ3倍の速さです。
機能
クラスとファクトリー関数にはそれほど機能に違いはなかったのですが、ES6になってからは違います。現在、ファクトリー関数とクラスはプライベートなデータを使えます。ファクトリー関数ではクロージャーで、クラスではWeakMapを用います。どちらも多重継承ができます。ファクトリー関数、クラスのどちらも必要であれば任意のオブジェクトを返せます。また、どちらもシンプルなシンタックスを提供しています。
最後に
すべての要素を勘案すると、クラスシンタックスが私の好みです。クラスシンタックスはスタンダードで、シンプルかつ簡潔で動作も速く、かつてはファクトリーしか提供できなかったすべての機能を提供してくれます。
(原文:JavaScript Object Creation: Patterns and Best Practices)
[翻訳:中村文也]
[編集:Livit]