2007年 12月 05日

JavaScript のコンストラクタ関数

new つけてもつけなくてもいいようにするにはどう書くのがベストか的なんだろう。Deferred ではこうしたけど

function Deferred () { return (this instanceof Deferred) ? this.init(this) : new Deferred() }
// prototype.init では this をかえしてる。
// でもコンストラクタは Object をかえした場合にしかその返り値を使わないので undefined でもべつにいい。

汎用的にするなら

function Foo () {
    if (this instanceof arguments.callee)
       this.init(this);
    else
       return new arguments.callee;
}

かな。ただ、これだと任意の数の引数がとれない (new は括弧をつけなくていいことからも解るように関数呼びだしではないので)。

function Foo () {
    if (this instanceof arguments.callee) {
       this.init.apply(this, arguments);
    } else {
       // new のエミュレート
       var f = function () {};
       f.prototype = arguments.callee.prototype;
       var o = new f;
       arguments.callee.apply(o, arguments);
       return o;
    }
}

どうみても素直に引数リストを書いたほうがよみやすくていい。(instanceof (がつかっている [[HasInstance]]) は [[Prototype]] をみる。new 以外に [[Prototype]] をセットする方法はない。)

てかはてなで

[[Prototype]]

[] [[Prototype]] [] <- リンクはされないけど括弧が消える。

を地で書くにはどうしたらいいんだ……

ちなみに global ( (function () { return this })() ) と、this をくらべるのはよくないことがある。

jQuery Deferred だと $.deferred = Deferred とやっているわけだけど、この場合 this は $ (jQuery) になってしまうのでだめ。

Deferred

jQuery Deferred とか言っていますがコアは全く jQuery に依存しないので (いい名前がおもいつかないから jQuery バインディングのほうのなまえでよんでる) http://svn.coderepos.org/share/lang/javascript/jquery-deferred/jquery-deferred.js の Deferred 関数と必要な関数をコピペしたら GM でもうごきます。setTimeout/clearTimeout にだけ依存してるのでその実装があるならどこでもうごくはず。

他の非同期なやつを Deferred 化するのは jQuery Deferred だと

function wait (n) {
	var d = new Deferred(), t = new Date();
	var id = setTimeout(function () {
		clearTimeout(id);
		d.call((new Date).getTime() - t.getTime());
	}, n * 1000)
	d.canceller   = function () { try { clearTimeout(id) } catch (e) {} };
	return d;
}

こんな感じになってます。Deferred#call がコールバック起動で、Deferred#fail がエラーバック起動です。

MochiKit のコードだと (機能がちょっと違いますが)

    /** @id MochiKit.Async.wait */
    wait: function (seconds, /* optional */value) {
        var d = new MochiKit.Async.Deferred();
        var m = MochiKit.Base;
        if (typeof(value) != 'undefined') {
            d.addCallback(function () { return value; });
        }
        var timeout = setTimeout(
            m.bind("callback", d),
            Math.floor(seconds * 1000));
        d.canceller = function () {
            try {
                clearTimeout(timeout);
            } catch (e) {
                // pass
            }
        };
        return d;
    },

引数 (value) をわたすためにチェインつかってるのがおもしろいすね。


ついでに jAutoPagerize でつかっているやつ。

function CachedResource (uri, convertfun, expire) {
	var d   = Deferred(); // new なしでいける。
	var key = uri;
	var v   = {};
	try { v = eval(GM_getValue(key)) || ({}) } catch (e) { log("parse error: may be uneval bug") }
	d.clear = function () {
		GM_setValue(key, "");
		return this;
	};
	if (v.time && v.time > (new Date).getTime() - expire) {
		log("Cache Hitted: " + key);
		setTimeout(function () { d.call(v.body); }, 10);
	} else {
		log("Cache expired; getting... " + key);
		GM_xmlhttpRequest({
			method  : "GET",
			url     : uri,
			onload  : function (req) { try {
				var res = convertfun(req.responseText);
				GM_setValue(key, uneval({time:(new Date).getTime(), body:res}));
				log(key, uneval({time:(new Date).getTime(), body:res}));
				log("Cached: " + key);
				d.call(res);
			} catch (e) { d.fail(e) } },
			onerror : function (e) {
				d.fail("HTTPError:"+e);
			}
		});
	}
	return d;
}