ファクトリ関数 [JavaScript]
もりけん塾(@terrace_tech)にて、JavaScriptを勉強しています。
今回は、ファクトリ関数について調べていきたいと思います。
ファクトリ関数を学ぶ必要がある理由
私は以前、JavaScriptを新しく勉強している人の意見で「クラスによる新しい書き方があるから、ファクトリ関数などの古い書き方は覚えなくてもいいと思う」というものを目にしたことがあります。
その時感じたのは、果たして新しい書き方だけを学ぶだけで本当に充分なのだろうか、という気持ちでした。
きっと実際の現場に入っていくと、クラス以外にもこういった何年も前からある書き方のコードにも触れる機会があるのではないかということです。
実際MDNのJavaScriptのクラスの説明のページにも、クラスはJavaScript にすでにあるプロトタイプベース継承の糖衣構文であると明記してあります。
なので、これから先色んなコードを扱っていくためにも、まずは従来からあるファクトリ関数についてきちんと理解しておきたいと思うようになりました。
また今回調べていて分かったのですが、ファクトリ関数は多くのサイトで「thisやnewに依存しない、一番望ましいオブジェクトの生成の仕方」として紹介されています。
単純ではありますが、そこまで多くの人がそういうのであれば、きっとファクトリ関数には多くの利点があるのではないかと思うようになりました。
果たしてそれが本当に正しい情報であるかを自分自身で見極めるためにも、まずはよく調べてみる必要があるなと感じました。
ファクトリ関数とは
ファクトリ関数とは、オブジェクトを返す関数のことです。
コンストラクタ関数と同じように、オブジェクトを作成するためのひな型の役割をします。
ただコンストラクタ関数と違う点は、オブジェクト作成時にnewキーワードを付けて呼び出す必要がありません。
newを付けてコンストラクタ関数を呼び出すと、作られたインスタンスはコンストラクタ関数内のthisと関連付けられ、__proto__の中にprototypeを割り当てられます。
ファクトリ関数には、その動きはありません。なので、ファクトリ関数内でthisを設定する必要もありません。
ファクトリ関数の設定の仕方
コンストラクタ関数では、このように関数を設定しました。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; }
ファクトリ関数の場合は、このように設定します。
function createBook(title, author, year) { return { title: title, author: author, year: year }; }
ES6の書き方だと、こうなります。
function createBook(title, author, year) { return { title, author, year }; }
「ファクトリ関数とは」にも書いた通り、ファクトリ関数は、単純にオブジェクトを返す関数です。
return { }
の部分が、オブジェクトを返す動作です。
これがもし仮に配列を返すのであればreturn [ ]
と書かれます。
ファクトリ関数を使ってオブジェクトを作成する
オブジェクトを作成する際は、下記のように関数を呼び出します。コンストラクタ関数の時と似ていますが、ファクトリ関数の場合はnewを付けて呼び出す必要はありません。
const book1 = createBook("人間失格", "太宰治", 1948);
ファクトリ関数のメソッドの作り方
コンストラクタ関数の場合、コンストラクタ関数名.prototype.メソッド名
でメソッドを作成し、prototypeを設定します。そしてnewを付けてコンストラクタ関数を呼び出すことで、自動的に_proto_を介してprototypeにアクセスすることが出来ました。
ファクトリ関数の場合は、prototypeとして作成した関数ではなくても、手動でprototypeとして設定することが出来ます。
やり方は色々ありますが、今回は2つの方法を見ていきます。
Object.setPrototypeOf()
最初に、Object.setPrototypeOf()のやり方で書いてみます。
まず、prototypeとして使用したいメソッドのprotoBook()
を作成します。
次にcreateBook()
内で、setPrototype()
を使って、メソッドprotoBook()
をprototypeに設定します。
const protoBook = { calcYear: function () { return `この本は発行から${2020 - this.year}年経っています`; }, }; function createBook(title, author, year) { var newBook = {}; newBook.title = title; newBook.author = author; newBook.year = year; return Object.setPrototypeOf(newBook, protoBook); }
関数の設定が終わったら、ファクトリ関数createBook()
を呼び出し、オブジェクトを作成します。
const book1 = createBook("人間失格", "太宰治", 1948);
作成されたオブジェクト book の中身を見てみると、_proto_にcalcYear
が設定されていることが分かります。
メソッドcalcYear
を使うためには、下記のように実行します。
Object.create()
上で見たように、setPrototypeOf()でも、prototypeの設定は出来ました。
しかしMDNのドキュメントを読んでみると、setPrototypeOf()
は動作が遅いので、prototypeを設定する際にはObject.create()
を使う方が良いと書いてありました。
なので、setPrototypeOf で設定したやり方を、今度はObject.createの方法に書き直します。
Object.createもsetPrototypeOf同様、prototypeを指定してオブジェクトを生成する際に使われます。
下の例では、newBookオブジェクトを作る際に、protoBook
をprototypeとして指定しています。
const protoBook = { calcYear: function () { return `この本は発行から${2020 - this.year}年経っています`; }, }; function createBook(title, author, year) { const newBook = Object.create(protoBook); newBook.title = title; newBook.author = author; newBook.year = year; return newBook; }
同じくオブジェクトを作成し、_proto_の中身をチェックしてみます。
calcYear
が入っていました。実行してみます。
ファクトリ関数の this は何を指すか
ファクトリ関数内で変数を参照する際は、thisを使う必要はありません。 ですが、prototypeを別に設定する場合は、thisを使う必要があります。 その場合thisは、ファクトリ関数から返されたオブジェクトを指します。
const protoBook = { printThis: function () { console.log(this); }, }; function createBook(title, author, year) { const newBook = Object.create(protoBook); newBook.title = title; newBook.author = author; newBook.year = year; return newBook; }
オブジェクトを作り、this を表示させるメソッドprintThis
を実行してみます。
ファクトリ関数を使う利点
渡されるパラメーターによって、返すオブジェクトが変えられる
ファクトリ関数を使うと、関数を呼び出す際のパラメーターによって異なったオブジェクトを返す設定をすることが出来ます。
その特徴を使い、入金ボタンを押すと「入金オブジェクト」が生成され、出金ボタンを押すと「出金オブジェクト」が生成されるコードを書いてみます。
下記の例では、
入金のボタンにはカスタムデータ属性data-type="inc"
、出金のボタンにはカスタムデータ属性data-type="exp"
と設定してあります。
<input type="text" class="input__value" placeholder="金額を入力" /> <button class="btn" data-type="inc">INCOME</button> <button class="btn" data-type="exp">EXPENSE</button>
もし入金ボタンが押された場合は、ファクトリ関数budget内のcreateInc()
が実行される(=入金オブジェクトが返される)ようにし、
もし出金ボタンが押された場合は、ファクトリ関数budget内のcreateExp()
が実行される(=出金オブジェクトが返される)ように設定します。
const budget = function(type, price) { const createInc = function() { return { type: "入金", price }; }; const createExp = function() { return { type: "出金", price }; }; if (type === "inc") { return createInc(type, price); } if (type === "exp") { return createExp(type, price); } };
この2つのボタンにイベントリスナーを付け、カスタムデータ属性から読み込んだタイプinc
、exp
に応じて、budget関数内で別々の関数を呼び出させます。
document.querySelectorAll(".btn").forEach(function(target) { target.addEventListener("click", function(e) { const inputValue = document.querySelector(".input__value").value; const type = e.target.dataset.type; const item = budget(type, inputValue); console.log(item); }); });
console.logした結果がこちらです。(inputにそれぞれ1000と入力してからボタンを押しました。)
受け取ったtypeパラメーターごとに、それぞれ別々のオブジェクトを返すことが出来ました。
ファクトリ関数のクロージャのおかげで、変数を閉じ込められる
ファクトリ関数が実行された後、関数内のスコープは閉じられます。なのでファクトリ関数内で定義されたメソッドを実行した際には、スコープ内の変数が参照され続けます。
その動きを確かめるために、下記のようなコードを用意しました。
ファクトリ関数createBookの中のメソッドread
が実行されるたびに、1ずつ数字が増えていきます。
let pages = 10; function createBook(title) { let pages = 1; let read = function () { console.log(`${title}を${pages++}ページ読みました。`); }; return { title, read }; }
関数の外にも変数pages
がありますが、readメソッドを実行しても、そちらの変数は参照されません。
なぜなら、ファクトリ関数内createBook() が実行された後、createBook内のスコープは閉じられ、グローバルのpagesへの参照はもう残っていないからです。
book1.read()
を実行するたび、ファクトリ関数内のpages
が参照され、結果pages
は1ずつ増えていきます。
thisがundefinedになるトラブルが避けられる
コンストラクタ関数を使用した場合、関数の階層が深くなってくると、thisの値がundefinedになってしまうことがあります。
ファクトリ関数はそもそもthisを使用する必要がないため、そういったトラブルを回避することが出来ます。
どのような現象か、サンプルを見ながらチェックしていきます。
<コンストラクタ関数の場合>
関数の入れ子が深くなり、setTimeout関数の中の this が undefined になってしまっています。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; this.asyncTitle = function() { setTimeout(function() { console.log("この本のタイトル", this.title); }, 1000); }; }
<ファクトリ関数の場合>
ファクトリ関数の場合、変数名で参照できるので、コンストラクタ関数の時に起きたような「thisの値が不明」といったようなトラブルが起こりません。
function createBook(title, author, year) { const newBook= {}; newBook.title = title; newBook.author = author; newBook.year = year; const asyncTitle = function () { setTimeout(function () { console.log('この本のタイトル:', title); }, 1000); }; return { newBook, asyncTitle } }
変数名の衝突が防げる
ファクトリ関数ではreturnしたオブジェクトやメソッドだけが外部から参照出来ます。
なので、例えば同じ名前の変数名を各ファクトリ関数内で使用しても、returnしていないものに関しては外部からはアクセス出来ないので、名前の衝突が起きません。
下記の例ですと、createBook
とbookBought
はどちらもcount
という変数を使用していますが、returnしているのはあくまでメソッドだけなので、変数自体はファクトリ関数の中のスコープに守られています。
function createBook(title) { let count = 1; const read = function () { console.log(`${title}を${count++}ページ読みました。`); }; return { title, read }; } function bookBought() { let count = 1; const buy = function () { console.log(`今月${count++}冊目の本を買いました。`); }; return { buy }; }
ファクトリ関数内で加工したデータを返すことが出来る
ファクトリ関数を使うと、外部に返すデータを指定することが出来ます。
例えば何かの計算結果などのデータを使いたい場合は、計算するメソッドそのものではなく、メソッドで加工済みのデータを返すことが出来ます。
なので、関数内にメソッドを定義したとしても、オブジェクトを作成した時に毎回メソッドを付与しなくてもよくなります。
コンストラクタ関数で同じく、コンストラクタ関数内にメソッドを定義すると、インスタンスを作るごとにメソッドが付与されてしまいます。これだと望ましくないので、メソッドを使いたい場合は、prototypeを別途定義する必要が発生します。
実際に動きを見るために、「本が発行から何年経っているかを表示させるメソッド」を設定し、2つの関数の違いを確認してみます。
<コンストラクタ関数の場合>
コンストラクタ関数内でメソッドを設定してみます。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; this.published = function() { return `${2020 - year}年前`; } }
生成されたインスタンスには、publishedメソッドが付与されました。
publishedメソッドは下記のように実行することが出来ます。
ですが、インスタンス作成時に毎回メソッドが追加されてしまうのはよい状態ではありません。
なので「この本が発行から何年経っているかを計算する」といった関数を持たせたい場合は別途メソッドを作成し、インスタンスに持たせるのではなくprototypeとして設定します。
function Book(title, author, year) { this.title = title; this.author = author; this.year = year; } Book.prototype.published = function() { return `${2020 - this.year}年前`; }
インスタンスのprototypeに、publishedメソッドが設定されました。
以下のように書けば、publishedメソッドが実行出来ます。
<ファクトリ関数の場合>
同様に、ファクトリ関数内でメソッドの関数を設定してみます。
function createBook(title, author, year) { const howOld = function () { return `${2020 - year}年前`; }; const published = howOld(); return { title, author, year, published }; }
ファクトリ関数の場合だと、メソッドの実行結果のみを返すことが出来ます。なので、たとえファクトリ関数内にメソッドを設定したとしても、オブジェクトを作成するたびにメソッドをオブジェクトに付与させる必要がありません。
ファクトリ関数内createBookを呼び出し、オブジェクトを作成します。
publishedメソッドが付与されなくても、publishedメソッドの計算結果が得られています。
まとめ
ファクトリ関数について学んだことで、「すべてのものをオブジェクトとして捉えるコーディングの仕方」といったものへの理解が少し進んだように感じます。
そしてクロージャや即時関数など、まだまだ理解が不十分なトピックも洗い出されてきました。それらについても調べていきつつ、ファクトリ関数を使用した、オブジェクト単位にまとまったすっきりしたコーディングにもどんどん慣れていきたいと思いました。
参考にしたサイト
Factory Functions and the Module Pattern | The Odin Project
ファクトリ関数の概要が簡潔にまとめられています。
より堅牢なコードを作成するためのJavaScriptのベストプラクティス—オブジェクトの作成 |ICHI.PRO
こちらも、ファクトリ関数の概要と、実際のコーディングのサンプルが記載されています。
ライブラリなしのJavaScriptで作る、関数ファクトリを使ったCounterのサンプル | Crudzoo
ファクトリ関数でのコーディング方法を、簡単なカウンターを作成しながら解説しています。
JavaScriptを教えていただいている、もりた先生の詳細はこちらです。
[もりけん塾]
ブログ:http://kenjimorita.jp
Twitter:https://twitter.com/terrace_tech