Direct Proxiesとは
Direct ProxiesってのはES6の機能の一つで、オブジェクトをラップして様々なタイミングで任意の処理を差し込めるというとっても夢広がリングな機能。
harmony:direct_proxies [ES Wiki]
例えば、プロパティのget/set時に処理を差し込むのはこんな感じ。
(Direct Proxiesは現状だとFirefoxでしか動かない。そして下記のコードは無駄にテンプレートリテラル使ってるのでFirefox 34以上じゃないと動かない)
// Proxyを使って処理を書き換える対象のオブジェクト
var target = { foo: 'bar' };
// Proxyを使ってtargetに対してhandlerを適用したproxyオブジェクトを得る
var proxy = new Proxy(target, {
// プロパティ読み取り時に処理を実行される処理
get(target, name) {
if (name in target) {
// 存在しているプロパティならそのまま返す(プロパティ読み取りのデフォルトの動作)
return target[name];
}
else {
// 存在していないプロパティの場合は適当なメッセージを返す
return `${name}は定義されてないよ!`;
}
},
// プロパティ書き込み時に処理を実行される処理
set(target, name, val) {
if (typeof val === 'string') {
// setしようとしている値が文字列であればそのまま保存する(プロパティ書き込みのデフォルトの動作)
target[name] = val;
}
else {
// 文字列以外の場合はメッセージを出力して何もしない
console.log('文字列しか受け付けないよ!');
}
},
});
console.log(proxy.foo); //=> bar
console.log(proxy.hoge); //=> hogeは定義されてないよ!
proxy.hoge = 1; //=> 文字列しか受け付けないよ!
console.log(proxy.hoge); //=> hogeは定義されてないよ!
proxy.hoge = 'fuga';
console.log(proxy.hoge); //=> fuga
Proxy
の第一引数に対象のオブジェクト(target
)、第二引数に差し込む処理(handler
)を指定する。このとき、target
自身にフックを仕込むんじゃなくて、新たな代理オブジェクト(proxy
)を作る。この点についてはObject.observeとかと挙動が違うので注意。
get/set以外にもめっちゃいっぱい処理が差し込めるポイントは用意されているので詳しくはwikiを参照のこと。
method missingとは
このDirect Proxiesが使えるように色々やれることが増えるんだけど、例えばRubyのmethod_missing
みたいなことができるようになる。(ちなみにFirefoxには__noSuchMethod__という同じような機能があるんだけど非標準である)
method_missing
ってのは、classにmethod_missing
っていう名前のメソッドを定義しておくと、オブジェクトに対して存在しないメソッドを呼び出した時、NoMethodError
にならずにmethod_missing
が呼ばれるというものだ。
class Foo
def method_missing(method_name, *args)
puts "#{method_name}は存在しないよ!"
end
end
Foo.new.hoge #=> hogeは存在しないよ!
Rubyのmethod_missing
は、例えばRailsのfind_by_xxx
みたいやつで使われている。DBのカラムにname
とage
があったら、RailsのModelは次のようなメソッドを利用できるようになる。
User.find_by_name 'foo'
User.find_by_age 30
この機能はmethod_missing
を使って実装されている。
今回はこの機能をDirect Proxiesを使ってBackboneに実装してみる。
ちなみにRailsのこの機能はRails4でdeprecatedになっていて、個人的にも動的にメソッドを呼び出すというのはちょっとアレな感じするのであまり推奨はしないけど、Direct Proxies使うとこんなことができるっていう例としてお楽しみいただければと。
Backboneに動的なfindメソッドを実装する
まずはmethod_missing
的なのを実装する。
var handler = {
get(target, name) {
// 存在しているプロパティならそのまま返す
if (name in target) {
return target[name];
}
// methodMissing関数が実装されていれば呼び出す
if (typeof target.methodMissing === 'function') {
return target.methodMissing(name);
}
}
};
これをhandler
に指定して、methodMissing
関数を実装したオブジェクトをtargetとしてProxy
を呼び出せばいい。
var obj = new Proxy({
methodMissing(name) {
return () => `${name}は存在しませんよ!`;
}
}, handler);
console.log(obj.foo()); //=> "fooは存在しませんよ!"
ちなみにこのmethodMissing
関数は、メソッドがなかったときに呼び出されるんではなく、プロパティがない場合に呼び出されるので正確にはmethodMissing
というpropertyMissing
のほうが正しいが今回は気にしないでおこう。そしてプロパティアクセス時なので、method_missing
的な動作にするために、methodMissing
では関数を返す必要がある。
これをBackbone.Collectionに適用すると次のようになる。
var CollectionBase = Backbone.Collection.extend({
constructor() {
Backbone.Collection.apply(this, arguments);
return new Proxy(this, handler);
},
methodMissing(name) {
var matched = name.match(/^findBy([A-Z][0-9A-Za-z]+)$/);
if (!matched) return;
var attrName = matched[1].charAt(0).toLowerCase() + matched[1].slice(1);
if (!(attrName in this.model.prototype.defaults)) return;
return (val) => this.findWhere({ [attrName]: val });
}
});
methodMissing
ではメソッド名をチェックしてfindByXxx
っていうメソッドかつ、Xxx
の部分がModelのdefaults
に存在しているattributeだったら検索メソッドを実行する関数を返すようにしている。
そしたら上記のCollectionを継承して適当なクラスをつくる。
var MyModel = Backbone.Model.extend({
defaults: {
name: null,
age: null,
}
});
var MyCollection = CollectionBase.extend({ model: MyModel });
そうするとMyCollection
でfindByName
やfindByAge
というメソッドが利用できるようになる。
var coll = new MyCollection([
{ name: 'foo', age: 20 },
{ name: 'bar', age: 30 },
{ name: 'baz', age: 40 },
]);
console.log(coll.findByName('bar').get('age')); //=> 30
console.log(coll.findByAge(20).get('name')); //=> foo
また、Modelのdefaults
に存在しないattributeの名前で呼び出したらちゃんとエラーになる。
coll.findByFoo(); // => TypeError: coll.findByFoo is not a function
Direct Proxiesが使えるようになると、他にも色んなことができるようになると思う。この前のES6+カジュアルトークで発表されていた、immutableなArrayを作るっていうのも面白かった。