Direct Proxiesでmethod missing的なことをやる


## Direct Proxiesとは

Direct ProxiesってのはES6の機能の一つで、オブジェクトをラップして様々なタイミングで任意の処理を差し込めるというとっても夢広がリングな機能。

[harmony:direct_proxies [ES Wiki]](http://wiki.ecmascript.org/doku.php?id=harmony:direct_proxies)

例えば、プロパティのget/set時に処理を差し込むのはこんな感じ。

（Direct Proxiesは現状だとFirefoxでしか動かない。そして下記のコードは無駄にテンプレートリテラル使ってるのでFirefox 34以上じゃないと動かない）

```javascript
// 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](http://wiki.ecmascript.org/doku.php?id=harmony:direct_proxies)を参照のこと。

## method missingとは

このDirect Proxiesが使えるように色々やれることが増えるんだけど、例えばRubyの`method_missing`みたいなことができるようになる。（ちなみにFirefoxには[\_\_noSuchMethod\_\_](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/noSuchMethod)という同じような機能があるんだけど非標準である）

`method_missing`ってのは、classに`method_missing`っていう名前のメソッドを定義しておくと、オブジェクトに対して存在しないメソッドを呼び出した時、`NoMethodError`にならずに`method_missing`が呼ばれるというものだ。

```ruby
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は次のようなメソッドを利用できるようになる。

```ruby
User.find_by_name 'foo'
User.find_by_age 30
```

この機能は`method_missing`を使って実装されている。

今回はこの機能をDirect Proxiesを使ってBackboneに実装してみる。

ちなみにRailsのこの機能はRails4でdeprecatedになっていて、個人的にも動的にメソッドを呼び出すというのはちょっとアレな感じするのであまり推奨はしないけど、Direct Proxies使うとこんなことができるっていう例としてお楽しみいただければと。

## Backboneに動的なfindメソッドを実装する

まずは`method_missing`的なのを実装する。

```javascript
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`を呼び出せばいい。

```javascript
var obj = new Proxy({
  methodMissing(name) {
    return () => `${name}は存在しませんよ！`;
  }
}, handler);

console.log(obj.foo()); //=> "fooは存在しませんよ！"
```

ちなみにこの`methodMissing`関数は、メソッドがなかったときに呼び出されるんではなく、プロパティがない場合に呼び出されるので正確には`methodMissing`という`propertyMissing`のほうが正しいが今回は気にしないでおこう。そしてプロパティアクセス時なので、`method_missing`的な動作にするために、`methodMissing`では関数を返す必要がある。

これをBackbone.Collectionに適用すると次のようになる。

```javascript
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を継承して適当なクラスをつくる。

```javascript
var MyModel = Backbone.Model.extend({
  defaults: {
    name: null,
    age: null,
  }
});

var MyCollection = CollectionBase.extend({ model: MyModel });
```

そうすると`MyCollection`で`findByName`や`findByAge`というメソッドが利用できるようになる。

```javascript
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の名前で呼び出したらちゃんとエラーになる。

```javascript
coll.findByFoo(); // => TypeError: coll.findByFoo is not a function
```

Direct Proxiesが使えるようになると、他にも色んなことができるようになると思う。この前のES6+カジュアルトークで発表されていた、immutableなArrayを作るっていうのも面白かった。

[ECMAScript6による関数型プログラミング](http://www.slideshare.net/TanUkkii/ecma-script6-40864841/12)