ECMAScript 5th 的 LazyGetter

ECMAScript 5th から正式に getter/setter が作れるようになったわけだが、これはMozillaJavaScript である SpiderMonkey では前々から実装されていた。

  • { get foo() { return ...; } }
  • { set foo(val) { return ...; } }

に加え、

  • Object.prototype.__defineGetter__
  • Object.prototype.__defineSetter__

が使えたわけだ。

で、これを使用したハックとして、以下のようなものがある。

function Foo () {
  this.__defineGetter__("bar", function(){
    var val = "BAR"; 
    delete this.bar;
    return this.bar = val;
  });
}
var f = new Foo();
Object.getOwnPropertyDescriptor(f, "bar");
/* => {
   configurable: true,
   enumerable: true,
   get: function () {
     var val = "BAR";
     delete this.bar;
     return this.bar = val;
   },
   set: undefined
  }
*/ 
f.bar; // "BAR";
Object.getOwnPropertyDescriptor(f, "bar");
/* => {
   configurable: true,
   enumerable: true,
   value: "BAR"
  }
*/ 

これは、プロパティbar[[Get]]されたときに、

  1. 値を取得
  2. getterを削除
  3. 同名のプロパティを設定

するというもの。遅延読み込みとして使える技だ。

が、しかし、上記コードでコンストラクタ上で定義したのは意味がある。

function Foo(){ }
Foo.prototype = {
  get bar() {
    var val = "BAR"; 
    delete this.bar;
    return this.bar = val;
  }
};

ということはできない。何故なら、

  • barプロパティはthisOwnPropertyにあるのではなく、プロトタイプ上にあるため、deleteできない
  • this.bar = valの代入演算子はプロトタイプをさかのぼってチェックするため
    • 左辺値への参照を探り、それがAccessorDescriptorであり、[[Set]]undefinedなので何もしない
    • Strictモードであれば、ここで例外を投げる

そんなわけで、コンストラクタ上にgetterを定義しなくてはならず、なんだか不恰好だし、コンストラクタが呼ばれるたびにgetterを定義するわけで非効率だなという印象を持っていた。


はい、前置き終わり。(長い前置きだ)

というわけで、ECMAScript 5th ですよ。上記問題点は代入演算子(=)がプロトタイプを遡る点(これはFirefox4 と Chrome, Safari の ES5 実装でも書いた)。
ならば、プロトタイプを遡らないObject.definePropertyを使えばいいじゃない。

function Foo () { }
Foo.prototype = {
  get bar() {
    var val = "BAR";
    Object.defineProperty(this, "bar", {
      configurable: true,
      enumerable: true,
      writable: true,
      value: val
    });
    return val;
  }
};

ECMAScript 5th、すばらしいですね。