はじめに
例えばD3.jsを使っていると以下のようなコードがたくさん現れます。
const manyBody = d3.forceManyBody()
.strength(200)
.distanceMax(400)
.distanceMin(60)
manyBody(0.5)
メソッドチェインでオブジェクトを初期化して、作成されたオブジェクトは関数として呼び出すことができます。
このようなオブジェクトを綺麗に実装するのが目標です。
従来の方法
いわゆるES5でのやり方は大雑把に以下の通りです。
var myPlugin = function () {
var plugin = function () {
// 関数として呼び出した時の処理
// ...
}
plugin.aMethod = function () {
// インスタンスのメソッド
// ...
return plugin
}
return plugin
}
まず plugin
の実体を返す myPlugin
関数で全体を括ります。
関数として呼び出した時の振る舞いを plugin
関数として実装します。
そして、 plugin
のメソッドを追加していき、最終的に plugin
を myPlugin
の戻り値として返します。
D3.jsの実装も大体このようになっています。
使い方は以下のようになります。
var p = myPlugin()
.aMethod()
p()
classを使った実装
最近のJSに慣れているとclassを使って実装したい気持ちになると思います。
まずは以下のように素直に実装します。
class MyPlugin {
constructor () {
// インスタンスの初期化
// ...
}
aMethod () {
// インスタンスのメソッド
// ...
return this;
}
execute () {
// 関数として呼び出した時の処理
// ...
}
}
const myPlugin = () => new MyPlugin()
さて、classを使ってしまうと作成したインスタンス p
を p()
のように呼び出すことはできません。
ここでは、代わりに execute
メソッドを追加し、その役割を持たせることにします。
また、最初のバージョンと同じ使い勝手にするために、インスタンスを生成する myPlugin
関数を作っておきます。
使い方は以下のようになります。
// const p = new MyPlugin() としてもOK
const p = myPlugin()
.aMethod()
p.execute()
これでも十分かなとは思うのですが、D3.jsでは特定のメソッドやプロパティを持っていて、なおかつ呼び出し可能なインスタンスを引数としてとるAPI(例えばd3-forceのプラグインを作るう場合など)が存在するため、単純なclassベースの実装では対応できません。
classとProxyを使った実装
さて、前述の課題を克服するために、いよいよ本題のProxyを使った実装を行います。
ProxyはES2015で追加された機能です。
(参照:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
最初に実装を示します。
class MyPlugin extends Function {
constructor () {
super()
// インスタンスの初期化
// ...
return new Proxy(this, {
apply (target, thisArg, argumentsList) {
return target.execute(...argumentsList)
}
})
}
aMethod () {
// インスタンスのメソッド
// ...
return this;
}
execute () {
// 関数として呼び出した時の処理
// ...
}
}
変更箇所は2箇所です。
1つ目は constructor
で this
をラップしたProxyインスタンスを返すようにしたことです。
Proxyのハンドラで apply
メソッドを定義することで、インスタンスの呼び出しをトラップすることができます。
2つ目は MyPlugin
が Function
を継承するようにしたことです。
これは、Proxyで apply
をトラップするためには、ターゲットオブジェクトが呼び出し可能である必要があるためです。
apply
メソッドの第1引数の target
は、Proxyでラップされていない MyPlugin
インスタンスです。
そのため、 target.execute(...argumentsList)
と実装することで、p()
が p.execute()
と同じ動きになります。
MyPlugin
に execute
メソッドを持たせたくない場合は、クラス内ではなくてプレーンな関数としても問題ありません。
実は、Functionを継承すればProxyを使うことは必須ではありません。
ただし、Functionクラスを使用する場合、その実装を文字列として与える必要があり、使い勝手が良いとは言い難いです。
さて、これでES5版と同じように使うことができるようになりました。
const p = myPlugin()
.aMethod()
p()
おわりに
今回は触れませんが、従来版では外部からアクセスできないprivateなフィールドを作るという意味合いもありました。
ES2015クラスで同様のことを行う場合はWeakMapを使ったprivateなフィールドの実現などが可能です。
ただし、その際にはProxyでラップされたインスタンスとオリジナルのインスタンスの違いなどを意識しなければなりません。
この辺りはclass-fieldsの実装が進めばもう少し楽になるでしょう。