JavaScriptのプロパティの上書きを検知する

必要になった状況の説明は割愛するのだが、オブジェクトのプロパティの上書きを検知したくなった。

コードで説明すると、事前に次のように書いておいたものが

window.Foo = {
  alpha: function () { }
};

後からこうされる。

window.Foo = {
  beta: function () { }
};

alpha関数が定義されていることを前提にalpha関数を呼び出しても、未定義でエラーになってしまう。 両方がひとつのアプリケーションのコードならば、コードを修正すればよいだけなのだが、
混みいった事情で、後から上書きされることを前提になんとかしないといけなくなった。 ということでwindowオブジェクトのFooプロパティが上書きされるのを検知して、新しいオブジェクトにalpha関数をマージすることにした。

検知する方法について、いくつか検討したのでまとめておく。

Object.observe

Object.observeはES7(ES2016)で提案されているオブジェクトの変更を監視する仕組み( refs: Object.observe() - JavaScript | MDN )。

詳しい仕様は上のURLをみてもらいたいが、監視対象のオブジェクトが変更されたときにコールバックが実行される。 データバインディングを機能に持つJSフレームワークの開発に関わっている人にとってうれしい仕様だ。
今はオブジェクトの変更をハンドリングするためにフレームワークの中の人が苦労している。Vue.jsではsetterを上書きすることで、Angular.jsではdirty-checkingという仕組みで実現している。

ただ、V8(Chrome, Opera)でしか実装されていないのとユーザー定義ではないオブジェクト(例えば、windowやdocument)の変更は監視できないのでボツになった。 Chrome拡張を開発している場合やオブジェクトで名前空間を実現している場合(例えば Foo.Bar.alphaのようになっていて、Barが上書きされようとしているとき)は使える。

Proxy

ProxyはES6(ES2015)に含まれる仕様( refs: Proxy - JavaScript | MDN )

使い方自体はObject.observeと似ているが、大きく異なる点はコールバックが実行されるタイミングである。Object.observeが変更後にコールバックが実行されるのに対して、Proxyのコールバックはプロパティにアクセスしようとしたときに実行される。
これを使うとRubyのmethod_missingを使ったゴーストメソッドと同じことを実現できる。

上書きされた後よりも前に差し込んだほうがなんとなく安心するので使いたかったのだが、
実装されているのがSpiderMonkey(Firefox)とChakra(Edge)だけで、トランスパイラのBabelでも対応していないのでボツになった(refs: ECMAScript 6 compatibility table )。

Object.defineProperty

最終的にObject.definePropertyで定義できるsetterでハンドリングすることにした。上に書いたVue.jsが変更をハンドリングしている方法と同じである。Object.definePropertyはES5の仕様なので大抵のブラウザで実装されている(refs: Object.defineProperty() - JavaScript | MDN )。

コードにするとこんなかんじになる。力技だ。

Object.defineProperty(window, 'Foo', (function () {
  var Foo = {
    alpha: function () { }
  };
  return {
    get: function () {
      return Foo;
    },
    set: function (newValue) {
      // ここでなんとかする
      newValue.alpha = Foo.alpha;
      Foo = newValue;
    },
    enumerable: true,
    configurable: true 
  };
})());