Displaying posts filed under

JavaScript

doScrollによるDOMContentLoadedエミュレーションの落とし穴

今更こんなことが問題になるケースは稀だと思うが、『パーフェクトJavaScript』の中でも紹介されていたので注意として書いておく。

IE8以前でDOMContentLoadedイベントをエミュレートする方法として、doScrollを使ったハックは広く知られている。例として『パーフェクトJavaScript』230ページのリスト8.9より引用。

function IEContentLoaded (callback) {
    (function () {
        try {
            document.documentElement.doScroll('left');
        } catch (error) {
            setTimeout(arguments.callee, 0);
            return;
        }
        callback();
    })();
}

しかし実際に試してみればわかるが、これだとwindow.onloadより実行が遅くなる場合がある。

テスト1(画像なし) テスト2(画像あり)

具体的には

  • 画像など外部から読み込まれるリソースが少ない場合
  • リロードした時にキャッシュがきいている場合

こういったケースではwindow.onloadとdoScrollハックの実行順序は逆転する場合がある。もちろん逆転しない場合もある。doScrollハックはあくまでハックであって正式なイベントシステムの一部ではないのだから、一貫した動作をしなくても当然だと言える。

この実行順序の逆転を防ぐために、世の中のライブラリではdocument.onreadystatechangeイベントを併用するのが習わしとなっている。例えばdoScrollハックを世に広めたDiego Perini氏の実装を見れば、trying to always fire before onload というコメントとともにreadyStateをチェックするコードが入っているのが分かる。

document.onreadystatechange = function() {
    if (document.readyState == 'complete') {
        //コールバック関数を実行し、以後はdoScrollハックが動かないようにしておく
    }
};

テスト1(画像なし) テスト2(画像あり)

現実問題としては、わざわざ自前でdoScrollハックを書くような状況では、この順序の逆転が問題になることはまずないと思われる。しかし以下のような教訓を読み取ることはできるだろう:

  • 広く知られていようがハックはハック。使うときは慎重になろう
  • 可能な限り信頼できる既存の実装を使おう
  • 自分の手で実験しよう

jQuery 1.5の新機能: jQueryのサブクラスが作れるようになる

jQuery 1.5 beta1 のリリースノートを見ていたら面白そうな機能があったので紹介。

1.5正式版では jQuery.sub という名称に変更になったので、本文およびサンプルコードを修正しました。

jQuery.sub() でjQueryのサブクラスを作ることができる

var MyjQuery = jQuery.sub();

作成したサブクラスには独自のメソッドを定義することができる。定義の仕方、使い方は通常のjQueryと全く同じ。

MyjQuery.fn.writeHello = function(){ this.text('Hello World'); };
 
MyjQuery('p').writeHello();

通常のjQueryメソッドはサブクラスでもそのまま使える。サブクラスを作った後に追加したメソッドも使えるので、プラグインによる拡張と組み合わせても大丈夫。

MyjQuery('div').css('border', '1px red solid').writeHello();
 
//元のjQueryを拡張
jQuery.fn.pluginMethod = function(){ alert('OK'); };
 
//OK
MyjQuery('.klass').pluginMethod();

逆にサブクラスで追加したメソッドは元のjQueryからは見えないので、プロジェクトやプラグインに固有なメソッドを定義したり、元のjQueryにあるメソッドを上書きしたりといったことが安全にできる。

// text	メソッドを上書きする
MyjQuery.fn.text = function(val) {
  var orig = jQuery.fn.text;
  if(typeof val === "string") {
    return orig.call(this, "!!"+ val +"!!");
  } else {
    return orig.apply(this, arguments);
  }
};
 
// 上書きされたtextを呼び出す
MyjQuery('#ex').text("foo");
 
// 引数を与えなければ元のメソッドが呼ばれる
MyjQuery('#ex').text();	//!!foo!!
 
// もちろん元のメソッドには影響ない
jQuery('#ex').text('foo'); //foo

さらに応用として、プラグインで拡張メソッドを提供する場合の名前空間としても使えるかもしれない。例えば私が前に作った動的にSELECTを構築するためのjQueryプラグインは、グローバルなメソッド空間に2つのメソッドを追加してしまうのであまりマナーが良くない。

$('select').addOption(...);
$('select').clearOptions(...);
 
//本当は次のようにしたいが、動作しない
$('select').optionBuilder.addOption(...);
$('select').optionBuilder.clearOptions(...);
 
//次のような実装が推奨されている
$('select').optionBuilder('addOption', ...);
$('select').optionBuilder('clearOptions', ...);

1.5のサブクラス機能を使えば、次のように隔離されたサブクラスでのみ使用可能なメソッドとして提供することができる。

//サブクラスを使えば直接呼び出せる
var optionBuilder = jQuery.optionBuilder();
optionBuilder('select').addOption(...)
optionBuilder('select').clearOptions(...);
 
//関数の中に隔離する方法も
jQuery.optionBuilder(function($){
    $('select').addOption(...);
    $('select').clearOptions(...);
});

実装は次のような感じになるだろう。

/*
 * jQuery.optionBuilder.fn[method](...);
 * を
 * jQuery.fn.optionBuilder(method, ...);
 * にマップする
 */
jQuery.optionBuilder = (function(){
  var sub = jQuery.sub(),
      base = jQuery.fn.optionBuilder,
      slice = Array.prototype.slice,
      methods = ['addOption', 'clearOptions'];
 
  $.each(methods, function(i, method){
    sub.fn[method] = function(){
      return base.apply(this, [method].concat(slice.call(arguments)));
    };
  });
  return function(fn){
    if(typeof fn === 'function') {
      return fn(sub);
    }
    return sub;
  };
}());

参考:

Functionコンストラクタと関数式(function expression)の違いなど

JavaScript Patterns 2章ではevalの代替としてFunctionコンストラクタを使う方法が紹介されています。Functionコンストラクタは、普通にJavaScriptを書いている分にはなかなか使うことがないので、馴染みがない人も多いだろうと思います。そこで関数式との比較を中心に簡単にまとめておきます。

Functionコンストラクタは、関数オブジェクト(つまりJavaScriptにおける関数)を作るためのコンストラクタです。文字列をJavaScriptコードとして扱えるという点ではevalにも似ていますが、戻り値は普通の関数なので、その他の方法で作った関数と全く同じ使い方ができます。

var funcObj = new Function('x', 'y', 'console.log(x + y);');
 
//普通に呼び出す
funcObj(1,2); // 3
 
//コンストラクタとして使ってみる
funcObj.prototype.hello = function(){ console.log('World'); };
(new funcObj(4,5)).hello(); // 'World'

ちなみにコンストラクタではなく関数として呼び出した場合は、コンストラクタとして呼び出した場合と同じ結果になります(仕様上そう規定されています)。よって以下の2つは同じ意味になります。

var funcObj1 = Function('x', 'y', 'console.log(x + y);'),
    funcObj2 = new Function('x', 'y', 'console.log(x + y);');

この形、関数式にnew演算子を適用した場合と見た目が似ていますが、全く意味が違うということに注意してください。

// 関数式で関数を作る
var funcObj = function(){}
 
// 関数式で作った関数をコンストラクタとして呼び出す
var newlyCreatedObject = new function(){};

varの解釈について

Functionコンストラクタで作った関数においても、関数本体の中でvarステートメントを使って宣言した変数はローカル変数になります。

var a = 100,
    func1 = new Function('var b = 200; return b;'),
    func2 = function(){ var c = 300; return c; };
 
console.log(a); // 100
console.log(func1()); // 200
console.log(func2()); // 300
 
console.log(typeof b); //undefined
console.log(typeof c); //undefined

変数の生成(instantiation)は関数が呼び出されたときに実行されるプロセスなので、どのような方法で作った関数でも同じです。

スコープについて

関数式で作った関数は、関数式の外側のローカル変数を参照できます。しかしFunctionコンストラクタで作った関数はグローバル変数しか参照することができません。

//JavaScript Patterns Chapter2の例の改変
(function(){
    var local = 1;
    (function(){ local = 3; console.log(local); }()); // 3
    console.log(local); // 3
}());
 
(function(){
    var local = 1;
    Function("console.log(typeof local)")(); //undefined
}());

言い換えると、関数式で作った関数オブジェクトが「関数式が評価された時点でのスコープの状態」を保持しているのに対し、Functionコンストラクタで作った関数オブジェクトは常にグローバルなスコープしか保持していません。なぜこの差が生じるかというと、単に仕様でそう決まっているからです。関数に保持されるスコープは関数が作られたときに決定されるので、関数をどこでどのように作ったかによって結果が変わるということです。そして一度作られたあとは変更されることはありません。

この理屈によって、関数式ではクロージャを作ることができるが、Functionコンストラクタでは作ることができない、ということになります。

全くの余談ですが、クロージャが”閉じる”のは、スコープチェーンに組み込まれたActivation Objectがプログラマからは操作不可能だから、という事情によります。よってその他のオブジェクトをスコープチェーンに組み込んでやれば、クロージャと似ているが”開いた”関数を作り出すこともできます(役に立つかどうかは不明ですが)。

var func,
    scope = {alert: function(msg){ console.log(msg); }};
 
//スコープチェーンの中にオブジェクトを組み込む
with(scope) {
    func = function(x, y) { alert(x + y); };
}
 
//グローバルのalertは隠蔽されてconsole.logが実行される
func(1, 2);
 
//scopeのプロパティを削除することでグローバルなalertが実行されるようになる
delete scope.alert;
func(3, 4);
 
//再びalertを隠蔽する
scope.alert = function(){};
func(5, 6);

ECMAScript5の用語で言うと、Declarative Environment Recordに関数外部から束縛を追加したり削除したりはできないが、Object Environment Recordならbinding objectを通してそれが可能だということです。

参照

仕様書の中で関連する部分は多岐にわたるため、一部のみを挙げておきます。

  • 15.3 Function Objects – Standard ECMA-262 3rd/5th
  • 13 Function Definition – Standard ECMA-262 3rd/5th

またこの記事はazuさんのJavaScript Patterns読書記録を踏まえて書かれました。

Google AJAX Feed APIを使って複数のフィードをまとめて表示するjQueryプラグイン

Google AJAX Feed APIを使うとJavaScriptだけでRSS/ATOMフィードを読み込むことができますが、標準の機能では「複数のフィードをまとめて」「日付順に(新しい順に)」表示することができないので、jQueryプラグインを作りました。

jquery.googleMultiFeed.js (github)

タイマーを使って逐次処理を行っているので、全フィードの読み込みを待つことなく、読み込まれたものから順に表示されていきます。Google AJAX Feed APIの(というよりJSONPの)制約でフィードを並列的に読み込むことはできないのですが、Google側でキャッシュ&タイムアウト処理が行われているため、多数のフィードを読み込んでもさほどストレスなく表示されるはずです。

jQuery 1.4以上が必要です。jqueryの.jsファイルはscriptタグを使って静的に読み込んでください。どうしても google.load を使う必要がある場合は setOnLoadCallback の中で initGoogleMultiFeed() を呼ぶ必要があります。

<script type="text/javascript" src="http://www.google.com/jsapi?key=API_KEY"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
<script type="text/javascript" src="jquery.googleMultiFeed.js"></script>
<script type="text/javascript">
//google.load('jquery', '1.4');
 
google.setOnLoadCallback(function(){
  //jqueryをgoole.loadで読み込む場合のみ必要
  //initGoogleMultiFeed();
 
  $('#feed').googleMultiFeed({
    urls: ['http://example.com/rss', 'http://example.net/feed'],
    numEntries: 5
  });
});
</script>
...
<div id="feed"></div>

urls パラメータは必須で、読み込みたいフィードのURLを配列として指定します。 numEntries パラメータはオプションで、フィード一つあたりの件数です。

上の例を実行すると http://example.com/rss と http://example.net/feed から5件ずつフィードを読み込んで、新しい順に並び替えた上で、指定したタグの中に次のようなHTMLで表示されます。

<div>
<a title="エントリータイトル | フィードタイトル" href="リンク">エントリータイトル [フィードタイトル]</a>
<span class="entry-date">日付(YYYY-MM-DD)</span>
</div>
...(フィードの件数分繰り返し)...

表示されるHTMLをカスタマイズするには format パラメータでフォーマット用の関数を指定します。例えばエントリータイトルとリンクだけをリスト形式で表示したいなら、次のような関数を指定します。

//第一引数がエントリ、第2引数がフィード
//詳細は http://code.google.com/intl/ja/apis/feed/v1/reference.html#JSON
function titleOnly(entry, feed) {
  var link = $('<a />').attr({href: entry.link}).text(entry.title);
  return $('<li />').append(link);
}
 
//ULタグに対して追加する
$('ul#feed').googleMultiFeed({
  urls: ['http://example.com/rss', 'http://example.net/feed'],
  format: titleOnly
});

その他、loadCompleted パラメータにカスタム関数を指定すると、全てが読み込まれた後に”読み込み中インジケータ”を消す等の処理も行うことができます。有効なパラメータについてはGitHub上のWikiを参照してください。

以下のサイトなどを参考にしました。

Topsyのotter APIを使ってtwitter検索

Topsyが提供しているOtter APIを使ってTwitter検索。

特定のURLに対するツイート一覧を得る

特定のURLに対するツイートは trackbacks というリソースで取得できる。

例: http://wp.serpere.info/に対するツイートを10件までテキストフォーマットで得る

パラメータはtrackbackリソース固有のものと、共通のリスト用のものが使用できる。orderパラメータでレスポンスの順序を指定できるとあるのだが、現時点(2010-11-01)では正しく機能せず、何を指定しても常に新しい順で返ってくるようだ。

レスポンスの形式は拡張子で指定できる。デバッグ時はテキスト形式(.txt)が便利。実際にプログラムから使用する場合はJSON形式(.json)またはJSONP(.js)を使う。JSONの構造については今のところ実際に動かしてみて確認するしかない。

例: http://wp.serpere.info/に対する最新のツイート10件をjQueryによるJSONPで取得・表示する

otter API trackback resource – jsdo.it – share JavaScript, HTML5 and CSS

サイト全体に対するツイート一覧を得る

サイト全体(特定のドメイン以下のページ)に対するツイートは search リソースで site: クエリを使うことで得られる。

例: このサイト全体に対する言及ツイートを、過去1ヶ月分10件まで取得

使用できるパラメータはsearchリソースに対するものと共通のリスト用オプション。windowオプションでは対象となるツイートの期間を指定する。d10で10日、w2で2週間、aで全期間など。

このsearchリソースの仕様には色々と謎が多い。

  • 新しい順にソートして取得するためのsearchdateリソースを使った場合と、取得できる内容が大きく異なる。searchdateの方が反映が遅い?
  • 効かないはずのorderオプションが効いている? order=date に設定すると searchdate を使った場合と結果が同じになる?

例: このサイトに対する過去1か月の言及ツイートを20件、jQueryによるJSONPで取得し、自分(tkykmw)以外のものを表示

otter API search/searchdate resource – jsdo.it – share JavaScript, HTML5 and CSS

Kanasan.JS jQuery コードリーディング#2に参加しました

9月26日にKanasan.js主催で開催された「jQuery コードリーディング#2」に参加してきました。

複数人でペースを合わせてコードを読んでいくのは初めてだったので、最初は勝手がつかめなくて戸惑いましたが、慣れるとかなり面白かったです。一人で読んでいたら何気なく読み飛ばしてしまうような箇所でも、他の人の質問を受けて改めて読み返すと意外な発見があったりして、なかなか新鮮な体験でした。自分も途中からはほとんど独り言をつぶやくようなつもりで発言しまくっていました。

ただコードを読む前に、まずjquery.comのAPI Referenceを読むべきだったかなと思いました。たとえよく使うメソッドであっても、意外と知らない機能が隠れていたりするもので、そうなるとコードの意図を推測する時間が余計にかかります。例として jQuery.map が挙げられます。このメソッドは、JavaScriptの Array.prototype.map と違ってLispの mapcan みたいな機能を兼ねています。すなわち null, undefined は無視され、配列は concat されます。

$.map([1,2,3,4], function(x){ if(x % 2 == 0) return [x, x]; });
// => [2,2,4,4]

この動作を頭に入れておけば、最後の concat.apply は配列を一次元flattenするためのイディオムであることもすぐに見当がつきます。

ほとんど使ったことがないメソッドについては、この”意図を知る”作業は決定的に重要になります。今回の範囲で言うと私は queue, dequeue を全く使ったことがなかったので、かなり苦労しました。「これらはアニメーション(エフェクト)の基礎になっているメソッドだ」と別の方が指摘してくれたお陰でだいぶ助かりましたが、最初からAPIリファレンスを読んでいれば、もっと手っ取り早く正体がつかめたはずです。

実際にはこのqueque/dequeueは、非同期で実行される操作(関数)をあらかじめキューに溜めておいて、実行順序を保証するための仕組みです。アニメーション以外の例では、例えば jQuery.getScript によるスクリプトを読み込みを非同期かつ安全に実行するために使用できます。

/**
 * - LibraryA,
 * - LibraryA に依存する LibraryB
 * - LibraryA と LibraryB を使用する処理
 * を順番に実行する.
 * 読み込みが失敗したら後続の処理は実行しない
 */
 
// script-loader という名前の queue を使用する
jQuery.fn.loaderQueue = function(func) {
    return this.queue('script-loader', func);
}
jQuery.fn.loaderDequeue = function() {
    return this.dequeue('script-loader');
}
 
// スクリプト読み込みが成功したら dequeue する $.getScript
function dequeuingGetScript(url) {
    return function(next) {
        jQuery.getScript(url, function(data, status) {
                next();
            });
    };
}
 
jQuery(document)
    .loaderQueue(dequeuingGetScript('libraryA.js'))
    .loaderQueue(dequeuingGetScript('libraryB.js'))
    .loaderQueue(function(){ alert(LibraryA.func() + LibraryB.func()); })
    .loaderDequeue();

ただ実装の中ではエフェクト用の処理(fx queue)を決め打ちで特別扱いしているので、メソッドを分割した方が良さそうだよね、という意見が会場で出ました。

……

その他諸々、会場での話題や気づいた点など。

  • ローカル関数 access は非常に複雑かつ名前も分かりにくすぎる。特に第6引数 pass が何のためにあるのかわからない。一応
    $('#sss').attr({ key:function(){  }}, true)
    という形で使用すれば機能することはわかったが、何に使うか不明。
  • jQuery.support.hrefNormalized の名前が意味と逆転している。
  • expando って何?=>実行時に動的に追加されるプロパティのこと。
  • ローカル関数 eventSupported は特定のイベントがbubbleするかどうかのチェックに使われている。IEではsubmit/changeはbubbleしない。jQuery 1.4からはjQueryがエミュレートしてくれるようになった。
  • jQuery.data まわりのコードで jQuery.expando とローカル変数 expando の参照が入り交じっている。ケアレスミス?
  • 変数名 jQuery.cache は変。何かをキャッシュしているわけではなく、単なるデータストア。

あと懇親会ではバイクで行ったせいでお酒が飲めず、大変残念な思いをしたので、次に参加するときには絶対にバスか電車で行こうと思いました。

jQuery History is now on Github

I created a fork of jQuery History plugin on Github.

Though this is just a fork for now, Mikage Sawatari, the original author of this plugin, and I agreed that it could be the mainstream in the near future.

I’ve already done some major changes to the script structure:

  • It no longer uses the Webkit-specific hack for updating the location object, because recent versions of Webkit can correctly update the object just like Firefix and IE8.
  • Hashes are percent-encoded with encodeURIComponent before being put to location.hash. So you can use any characters such as `?’ in your hash.
  • history.load does not execute the callback unless hash is changed.
  • Updated API/code format. Thanks to Kevin Dalman.

It works on IE6, IE7, IE8, Firefox3, Safari4, Chrome4, and Chrome5.

If you have any problems or feature requests, please create a new issue in the ITS. And of course, you can create your own fork.


jQuery Historyプラグインは今後Githubで管理されることになりました。

Mikage Sawatariさんによる元々のバージョンに対し、次のような大きな変更が加えられています。

  • Webkitで動かすためのハックは不要になったので取り除かれました。
  • location.hash に代入する前に encodeURIComponent によるパーセントエンコーディングを行うようにしました。これによって ? を含むあらゆる文字を hash として保存できるようになりました。
  • ハッシュが変更されていない場合はコールバックが実行されないようになりました。
  • API/コーディングスタイルを変更しました。この変更はフォーラムのKevin Dalman氏の投稿に依ります。

IE6, IE7, IE8, Firefox3, Safari4, Chrome4, Chrome5で動作します。

Githubのバグトラッキングシステムに登録してください。もちろん自分でforkを作って修正していただいても構いません。

XPCNativeWrapperとunsafeWindowの間で同期処理を行う

先日使い方を学んだ MessageEvent と、location.href+javascript:ハックとJSDeferred userscript版とを組み合わせ、XPCNativeWrapper と unsafeWindow の間で同期処理を行うコードを書いてみた。

まずは MessageEvent を使って前回 evalInPage 相当の処理を作る。キャンセル処理も入れたかったので UnsafeWrapper というオブジェクトにまとめることにした。前回より長くなったように見えるが、実際には MessageEvent の lastEventId が使えるようになった分、簡潔になっている。

var UnsafeWrapper = new (function() {
	var seqId = 0, waiting = {}, self = this, noop = function(){};
 
	function dispatch(data, id) {
	    var e = document.createEvent("MessageEvent");
	    e.initMessageEvent('GM_UnsafeWrapper_returned', true, false,
			       data, location.protocol + "//" + location.host,
			       id, window);
	    document.dispatchEvent(e);
	}
 
	function listen(func) {
	    document.addEventListener('GM_UnsafeWrapper_returned', func, false);
	}
 
	listen(function(e) {
		if(waiting[e.lastEventId]) {
		    waiting[e.lastEventId].call(self, JSON.parse(e.data));
		    delete waiting[e.lastEventId];
		}
	    });
 
	this.exec = function(func, args, callback) {
	    var userFunc = "("+ func +").apply(null,"+ JSON.stringify(args || []) +")";
	    waiting[seqId] = callback || noop;
	    location.href = "javascript:void "+
	    (function(dispatch, ret, id) { dispatch(JSON.stringify(ret), id) }) +
	    "("+ dispatch +","+ userFunc +"," + JSON.stringify(seqId) +")";
	    return seqId++;
	};
 
	this.cancel = function(id) {
	    delete waiting[id];
	};
    });

そして Deferred に対するアダプタを書く。とても簡単。

with(D()) {
    function unsafeExec(func, args) {
	var d = new Deferred();
	var id = UnsafeWrapper.exec(func, args, function(ret) {
		d.call(ret);
	    });
	d.canceller = function(){ UnsafeWrapper.cancel(id) };
	return d;
    };
}

これで例えば、GM_xmlhttpRequest で外部ドメインからデータを取得して、そのデータを整形して、unsafeWindow 側で表示処理を行って、そしてその結果をuserscript側で受け取って…という処理は次のように書くことができる。

with(D()) {
 
xhttp.get("http://example.com/")
    .next(function(res) {
            var data = res.responseText;
            //....
 
	    return unsafeExec(function(data) {
                    $.each(data, function(k,v) {
                      //...
                    });
                    //...
		    return result;
		},
		[data]);
	})
    .next(function(result) {
          //...
	});
 
}

より実用性を高めるのであれば、location.href で実行するコード全体を try {} で囲み、catch したエラーをJSONに変換して _failed イベントを dispatch して、Deferred の fail に転送すれば、unsafeWindow 側で起こったエラーをuserscript側のDeferred chainで捕捉できるようになる。

参考:

XPCNativeWrapperとunsafeWindowの間でデータを送受信する

XPCNativeWrapperの外側でスクリプトを評価する関数。Firebugも同じことをしている。unsafeWindowを触らないので安全。

function evalInPage(fun) {
  location.href = "javascript:void (" + fun + ")()";
}

SmartLDR更新 – 素人がプログラミングを勉強するブログ

これは面白い!Function.prototype.toString が関数のソースコードを返すことを利用した、巧妙にして簡潔なハック。

次のように引数を渡せるように改良すればさらに強力になる(引数に渡せるのはJSON化可能な値のみ)。

function evalInPage(func, args) {
    var argStr = JSON.stringify(args || []);
    location.href = "javascript:void "+ func +".apply(null,"+ argStr +")";
}

GM_xmlhttpRequest を使って別ドメインから得たデータを、unsafeWindow 上のライブラリを使って表示する、といったコードが安全かつ自然に書けるようになる。

// データの取得は Greasemonkey で行う
GM_xmlhttpRequest({ method:"GET",
                    url: "http://example.com/api",
                    onload: function(response) {
                      var data = response.responseText;
                      //...
                      evalInPage(render, [data]);
                    }
                  });
 
// データの表示は unsafeWindow 上で行う
// jQueryなど unsafeWindow 上のライブラリが使える
function render(data) {
  $.each(data, function(k,v){
    //...
  })
}

これだけでも十分便利なのだが、「返り値が利用できない」「Firefoxのlocation.hrefは非同期で実行される」といった難点があるらしい。それを回避するためにunsafeExec on JSDeferred – 枕を欹てて聴くにおいてはJSDeferredというライブラリを使う方法が紹介されているのだが、コードを見る限り unsafeWindow に対するアクセスが必要になるようだ。

unsafeWindow にアクセスせずにGreasemonkeyに制御を戻すには、DOM Eventを使うのが良いと思う。以下、DOMNodeInserted イベントを使って実装してみた(返り値として利用できるのはJSON化可能な値のみ)。

追記: Constellationさんからコメントをいただきました。DOMNodeInsertedなどのイベントを使わなくても、直接createEvent/dispatchEventでイベントを生成・データを送受信する方法があるようです。 詳細はこのページの Constellationさんのコメントや、『Greasemonkey スクリプトとイベントで通信: Days on the Moon』など参照してください。

function getUniqId() {
    do {
        var id = "_tmp" + String(Math.random()).slice(2);
    } while(document.getElementById(id));
    return id;
}
 
function evalInPage(func, args, callback) {
    var argStr = JSON.stringify(args || []),
        userFunc = "("+ func +").apply(null,"+ argStr +")";
 
    if(!callback) {
        location.href = "javascript:void "+ userFunc;
        return;
    }
 
    var div = document.createElement('div'),
        id = getUniqId();
    div.id = id;
    div.style.display = 'none';
    document.body.appendChild(div);
    div.addEventListener('DOMNodeInserted', function(e){
            callback(JSON.parse(e.target.nodeValue));
            div.parentNode.removeChild(div);
        }, false);
 
    location.href = "javascript:void "+ (function(ret, id) {
            document.getElementById(id)
            .appendChild(document.createTextNode(JSON.stringify(ret)));
        }).toString() + ".call(null,"+ userFunc +"," + JSON.stringify(id) +")";
}

JSDreferredを使う方法と同じく、戻り値を受け取るコールバック関数を指定する。

evalInPage(function(obj, num) {
        //この中のコードは unsafeWindow で実行される
        return { answer: obj.a + num };
    },
    [{a:99}, 1],
    function(ret) {
        //この中のコードは Greasemonkey で実行される
	alret(ret.answer);
    });

多分まじめに作り込めば一種のRPCシステムみたいになると思う。

JavaScriptで確実にglobalオブジェクトを得る方法

確実にグローバル関数/変数にアクセスしたいとき。あるいはglobalオブジェクトの名前が分からないときに。

var global = (function(){ return this })();

ブラウザ上では window がglobalオブジェクトを指すことは暗黙の前提になっているが、そうでない環境もあり得る。

ちなみに Greasemonkey と Chrome user-script ではともに window でglobalオブジェクトにアクセスできるようだ。