素人がプログラミングを勉強していたブログ

プログラミング、セキュリティ、英語、Webなどのブログ since 2008

連絡先: twitter: @javascripter にどうぞ。

.sort.call(null)の深淵

発端は

javascript:alert([].sort.call(null)) これで window オブジェクト取れるのなんで?

http://twitter.com/edvakf/status/9222713572

という投稿。

この不可解な挙動を説明することは案外難しい。
まず、ES5のstrict mode以前(つまり、今普通にJavaScriptを使う場合)では、

function fun() {
  return this;
}

alert(fun.call(null) === window);

がtrueになる。

それから、

var a = [3, 2, 1];
alert(a.sort() === a);
alert(a); // [1, 2, 3]

sortは破壊的であり、thisを返す。

thisが配列以外の場合については、仕様では未定義であるが、どうなのか。id:nanto_viさんに指摘されたが、Array.prototype.sortは配列以外を受け取った場合、thisを返すと定義されていた。ただし、host objectと呼ばれる特殊なオブジェクトをthisにした場合(実装によるが、例えばDOMオブジェクトなど?)は環境依存であるようだ(15.4.4.11 Array.prototype.sort (comparefn)のNOTE2)。よって、windowをソートすることが未定義であるかどうかはwindowがhost objectであるかどうかに依存するが、windowがhost objectであるかどうかは仕様では示されていない(たぶん)。

var o = {};
[].sort.call(o)

オブジェクトにlengthがない場合は何もせずthisが返ってくる。

var o = {
  0: "b",
  1: "a",
  length: 2
};
[].sort.call(o)[0] == "a"; // true

lengthがある場合は配列以外でもきちんとソートされる。

windowの場合、

alert(window.length); // 0

lengthプロパティが存在する。window.lengthはドキュメントに含まれるframesの数を表している。
例えばフレームが一つあるサイトではwindow.lengthが1になる。
そして、window[0] == window.frames[0]になる。

なお、

window[0] = 1;
alert(window[0]); // 1

1がalertされることから分かるように、window[0]などは書き換え可能である。

さて、.sort.call(null) == windowについて考える。
まず、.sort.call(null)[].sort.call(window)と同じである。
そして、通常はwindow.length==0である。
windowをsortすると、sortすべき要素がないので何もせずthisを返す。
thisはwindowなので、window==windowとなり、trueになる。
これで説明ができた。

window.length==1の場合も同様に、sortすべきものがないので同じ結果となることが予想できる。
では、window.length>1の場合はどうなるのか。

var a = [10,2];
a.sort(); // [10, 2]

となることから分かるように、sortは引数で比較関数を渡さない場合は文字列に一旦変換されてから比較される。

つまり、window.lengthが2の場合、内部では
String(window[0])String(window[1])
を比較しているということになる。

framesのどれかがsame-originでない場合、toStringに失敗してエラーになる可能性があることが分かった。

さて、全てsame-originの場合はどうなってしまうのか。
WindowオブジェクトのtoString()は実装によって違うが、例えばSafariでは"[object DOMWindow]"を返す。

Array.prototype.sortが安定だとは限らないことを思い出す。
つまり、window.length>1の場合はwindow[0]が別のindexのframeに書き換えられる可能性があるということを意味する。
[].sort.call(null)は安全ではないのだ。

さて、そうは言ってもsortの実装は大抵安定なので本当に書き換えられることがあるのか?という疑問が出てくる(出てこないか)。

丁度実験に適したサイトを見つけてきたので試す。window.length==3のサイトだ。
ƒtƒŒ[ƒ€Žg—p—á

このサイト上で

var a = window[0], b = window[1];
[].sort.call(null);
alert(window[0] == a); // true
alert(window[1] == b); // true

を実行すると全てtrueとなり、やはり安定ソートだということがわかる。
これでは確認できないので、

window[0].toString = function () {
  return "b";
};
window[1].toString = function () {
  return "a";
};

を実行する。本当にsortされていれば、bがaより前になるはずである。
結果は、

[].sort.call(null);
alert(window[0] == b); // true
alert(window[1] == a); // true

予想通り。

結論:
[].sort.call(null)は様々な実装依存を含む汚れたハックコードなので、まともな用途には使わないようにしましょう。

それから、

(function () {
  var window = null;
  // ここでどうしてもwindowを取り出したい
})();

という場合は、ES5のstrict-mode以外では

(function(){return this}).call(null)

が使え、ES5のstrict modeではevalを間接的に呼んだ場合にcontextがglobalになることを利用して、

[eval][0]("window");

が使える。
上のコードが合っているかどうか自信が持てない。仕様を読んだところ、

eval.call(null, "window"); // もしくはeval.call(null, "this");

とするのが適切なようにも見える。

new Function("return window;")()

でも良い。
まあ、こんなコードが必要になること自体おかしい。