ククログ

株式会社クリアコード > ククログ > __noSuchMethod__をES6 Proxyで代替する方法

__noSuchMethod__をES6 Proxyで代替する方法

FirefoxのJavaScript実行エンジンであるSpiderMonkeyでは、ECMAScriptの仕様にはないSpiderMonkey固有の拡張機能をいくつか利用できますが、その中の1つとして__noSuchMethod__があります。

これはRubyなどでいう「メソッドミッシング」を実現するための仕組みで、あるオブジェクトに__noSuchMethod__という名前のメソッドを定義しておくと、呼び出されたメソッドが存在しなかった時に代わりに__noSuchMethod__メソッドが呼ばれるというものです。 具体的には以下のように使います。

// インスタンスに直接定義する場合
var object = {};
object.__noSuchMethod__ = function(name, args) {
  alert('NO SUCH METHOD: '+name+' , '+JSON.stringify(args));
};
object.toStringg('a', 'b'); // NO SUCH METHOD: toStringg , ["a","b"]

// クラスの一部として定義する場合
function MyClass() {
}
MyClass.prototype.__noSuchMethod__ = function(name, args) {
  alert('NO SUCH METHOD: '+name+' , '+JSON.stringify(args));
};

var instance = new MyClass();
instance.toStringg('a', 'b'); // NO SUCH METHOD: toStringg , ["a","b"]

こういった機能はいわゆるメタプログラミングにあたり、普段の開発で頻繁に利用する物ではありませんが、フレームワーク的な物を開発する場面では重宝します。 Ruby on Railsなどの「設定より規約」というルールでよく見られる「このような名前でパラメータを与えておけば自動的に、与えた名前に基づくこのような名前のメソッドが利用可能になる」という仕組みを、より自然な形で実現させられます。

ただ、前述の通りこれはSpiderMonkey固有の機能なので他のJSエンジンでは利用できませんし、SpiderMonkeyにおいてもFirefox 44で廃止されました。 代わりとして、より汎用的なProxyを使う事が推奨されています。

とはいうものの、Proxy__noSuchMethod__とは全く違う様式を取っており、しかも機能が豊富なので、単純に「__noSuchMethod__と同じ事だけをしたい」という場合にどうすればよいのか分かりにくいです。 また、__noSuchMethod__を使っていた箇所の設計を見直してProxyを適切に使うようにするとしても、それなりに規模が大きいコードの場合、フレームワーク的な基盤部分の設計を大きく変えてしまうと変更の影響範囲が大きくなって後が大変です。

結論を述べると、先のような例であれば、以下のようにしてProxy__noSuchMethod__を代替できます。

// インスタンスに直接定義する場合
var object = {};
object.__noSuchMethod__ = function(name, args) {
  alert('NO SUCH METHOD: '+name+' , '+JSON.stringify(args));
};
  // 追加箇所:ここから
object = (function(source) {
  var cache = {};
  return new Proxy(source, {
    get: function(target, name) {
      if (name in target)
        return target[name];
      return cache[name] || cache[name] = function(...args) {
        return target.__noSuchMethod__.call(this, name, args);
      };
    }
  });
})(object);
  // 追加箇所:ここまで
object.toStringg('a', 'b'); // NO SUCH METHOD: toStringg , ["a","b"]

// クラスの一部として定義する場合
function MyClass() {
  // 追加箇所:ここから
  var cache = {};
  return new Proxy(this, {
    get: function(target, name) {
      if (name in target)
        return target[name];
      return cache[name] || cache[name] = function(...args) {
        return target.__noSuchMethod__.call(this, name, args);
      };
    }
  });
  // 追加箇所:ここまで
}
MyClass.prototype.__noSuchMethod__ = function(name, args) {
  alert('NO SUCH METHOD: '+name+' , '+JSON.stringify(args));
};

var instance = new MyClass();
instance.toStringg('a', 'b'); // NO SUCH METHOD: toStringg , ["a","b"]

これまで__noSuchMethod__を使っていたコードに対して、例で「追加箇所」と記した部分を付け加えることで、同等の結果を得られるようになります。

ただし、厳密には全く同一というわけではありません。 __noSuchMethod__はメソッド呼び出しのみが対象になりますが、Proxyを使用したバージョンでは「未定義のプロパティにアクセスされたら、__noSuchMethod__を実行する関数オブジェクトを返す」という形になっているので、undefinedが返される事を期待して単純にvar hasProperty = !!instance.something;のようにした場合にも影響が出てしまいます。 この例であれば、"something" in instanceのような判別方法を使うという風に、「未定義のプロパティにアクセスするとundefinedだけでなく関数が返ってくる事もある」という前提で対象オブジェクトを処理するように気をつける必要がありますので、ご注意下さい。

また、例をよく読むと分かりますが、最初からProxyを使って書くのであればさらに無駄のない書き方もできます。 ここではあくまで、これまで__noSuchMethod__を使っていた既存のそれなりの規模があるコードに対して、最小限の変更で同様の結果を得られるようにするという目的に特化しているために、このようになっています。 このようなアドホックな対応で済ませるよりは、ProxyのAPI設計に即した使い方になるようにコードの設計を見直す方が基本的には望ましいと言えますので、実行に移すかどうかはメリットとデメリットをよく考えてから判断しましょう。