amachang - JavaScript 入門を最初に見たときは見落としてたんだけど、javascript - 勝手に添削 - JavaScript入門でwith()は徹底的に避けるべし
と言われている部分の奇妙なコードが気になって調べて見た。
var nl = document.getElementById('target02').getElementsByTagName('div');
for (var i = 0, l = nl.length; i < l; i ++) {
var e = nl[i];
with({e:e})
setTimeout(function() {
var box = new Box(e);
box.start();
}, i * 500);
}
amachang - JavaScript 入門
なんじゃこの with文? と思ったので色々試したり調べたりしてみた。
正しい挙動
withなし版
var nl = document.getElementById('target02').getElementsByTagName('div');
for (var i = 0, l = nl.length; i < l; i ++) {
var e = nl[i];
setTimeout(function() {
var box = new Box(e);
box.start();
}, i * 500);
}
えっ?と一瞬思った。
正しい挙動では、3つの■が順番に動くのに、withなし版では最後の■しか動かない。
でもよくよく考えれば当たり前だ。
感覚の目でよ~く見てみろ!
withなし版がsetTimeoutに渡したクロージャが使うeは、全部同じ実行コンテキスト上のeを参照している。しがたってsetTimeoutがクロージャを呼び出すときにはループは終わってeはnl[nl.length-1]を参照してしまっていて、みんなして最後の■を動かしてしまう。つまりキングクリムゾンによって過程はすっ飛ばされ、結果だけが残る
withを使うと、ループ毎に作られる無名オブジェクト{e:e}がスコープチェーンの先頭に追加されるので、withブロックの中では単にeと書くとブロックローカルなe(=nl[i])を参照できるというわけ。つまりwithを使ってCライクなブロックスコープを実現している。これは知らなかった。
参考:JavaScript でブロックスコープを実現する: Days on the Moon
http://d.hatena.ne.jp/amachang/20060910/1157911122
を見て。
たとえば、以下のようなソースの場合、hello と表示されるのはいつだろうか
setTimeout(function() { alert("hello") }, 10);
~ここに重くて、1秒の時間を有する処理を書く~
答えは 1000 ミリ秒後である。確実に 1000 ミリ秒後である。
おっしゃるとおり、僕の知ってる範囲内のJavaScript環境はすべて今実行している関数がブラウザに制御を返すまで、setTimeoutやsetIntervalに割り込まれることはないですね*1。
もっと言うと経験則的にはJavaScriptが実行中はブラウザの描画も行われません。だからinnerHTMLに大量のHTMLを突っ込むとかの重い処理を頻発するページは糞重い上に固まってるように見えます。
つまり、経験則的にはブラウザのレンダリング処理を含めてJavaScriptの実行はシングルスレッドで行われるように見えます。これが分かってないと重い処理にプログレスバーをつけようとしたときに、安直な書き方だと期待通りに動かなくてハマります。*2
この、JavaScriptがシングルスレッドなのって仕様で定められてるのですかね?
ECMAScriptの仕様を見れば載ってるのかもしれないけど、Cの仕様にシングルスレッドで動作するなんて書いてないようにECMAScript仕様にもそんなこと書いてないような気がしてちゃんと調べてないです。(「仕様に書いてない」ことを調べるのは面倒だし)
誰かちゃんとしたことを知ってる人がいたら教えて欲しい今日この頃です。
*1:経験的には、「ブラウザに制御を返すまで」なので、重箱の隅をつつくと上の例は1000ミリ秒後とは限らないですね。1秒の時間を要する処理が終わった後、return先がブラウザじゃなくてまた別な関数だったらもっと遅くなり得ます。
*2:一方描画も含めてシングルスレッドだから、JavaScriptの実行中にブラウザや他のスレッドがDOMツリーを変更したりするレースコンディションの可能性を考慮しなくて済むわけですが。

