ブログが続かないわけ

この日記のはてなブックマーク数
Webエンジニアが思うこと by junichiro on Facebook

[JavaScript]僕、スコープとかクロージャとか曖昧でした

このエントリーを含むはてなブックマーク hateb

JavaScript: The Good Parts を読みましたところ、いろいろと勉強になることがありました。

Douglas Crockford, 水野 貴明 ¥ 1,890
手っ取り早く習得
痒いとこだけ掻いてくれる
JavaScriptを勉強しなおすのにとってもよいです。
JavaScript コアに関する最高の本

付録を除くと100ページちょいという、とてもライトな本ですが、内容は濃密です。JavaScript の中で、一番わかりにくいだろうなと思われるようなところが集中的に解説されているように感じられ、勉強したてでかつそろそろわかりかけてきたなーと思い始めていた僕にとって、最適の一冊でした。知って良かったなと思えるところがたくさんありましたので、これから少しずつ紹介して行きたいと思いますが、今日はその中でも特に印象的だった、変数のスコープにまつわるお話をしてみたいと思います。

変数のスコープはその変数が宣言された関数の中だけに限定されます。なんとなく知ってはいたのですが、for 文とかではこういう風に書いてしまうことが多々ありました。

for (var i=0; i<array.length; i++) {
    // alert('test: ' + i);
}
ブロックレベルのスコープはないので、これは下記と同じことなんですね。
var i;
for (i=0; i<array.length; i++) {
    // alert('test: ' + i);
}
変数宣言はそのスコープをできるだけ狭める方がお行儀が良いので、他のプログラミング言語ではなるべく内側のブロック、例えばさっきの例ではfor 文のブロック内で宣言するのがよいとされているのですが、JavaScript では、関数内の先頭に書くのが一番わかりやすくなります。そうすれば、その関数全体がスコープになるというのが直感的にわかるようになると思います。

これを踏まえた問題をひとつ。これも、この本からの引用なので恐縮です。 下記のようなhtml が与えられたときに、h1 の「JavaScript」 をクリックしたら「1」を、h2 の「The Good Parts」 をクリックしたら「2」をalert するというような、そのnode が何番目のnode なのかをalert するというコードを考えるとします。

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="good_parts.css" rel="stylesheet" type="text/css" />
<title>JavaScript The Good Parts</title>
</head>
<body>
<h1>JavaScript</h1><h2>The Good Parts</h2><pre><script src="program.js"></script></pre>
</body>
</html>
【参考】 0番目のnode: body タグの直後の改行(テキストノード) 1番目のnode: h1 2番目のnode: h2 3番目のnode: pre 4番目のnode: pre タグの直後の改行(テキストノード)

program.js

var add_the_handlers = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function () {
            alert(i);
        }
    }
};
add_the_handlers(document.body.childNodes);
これは間違いの例です。僕はパッと見ただけではこのコードの問題点がわかりませんでした。 実は、上記のjs では、どちらをクリックしても4 がalert されてしまいます。4 というのはノードの総数です。

これはクロージャの概念と変数のスコープがわかっていればわかることらしいので、本では詳細が書かれておりませんでした。そこで、僕なりの解釈を付け加えてみたいと思います。このfor 文のi=1 の時には、「1番目のnode(つまりh1) のonclick イベントに function() { alert(i); } を割り当てろ」という命令がなされています。これで、h1 がonclick イベントに反応する準備ができました。そして、実際に、h1 がクリックされると、その際に実行されるのは、alert(i) です。では、このalert(i) が実行されたときに、i の値はどうなっているでしょうか。onclick を割り当てたタイミングでは1 だったかもしれないi の値ですが、実際にはにfor 文がnodes の総数分実行されたあとなので、i = 4 になってしまっています。その結果、alert(i) で、「4」がalert されてしまうということになります。

では、どのように直せばよいのでしょうか。

var add_the_handlers = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function (i) {
            return function () {
                alert(i);
            }
        }(i);
    }
};
add_the_handlers(document.body.childNodes);
このように、「関数を返す無名関数をすぐに実行する」という少しわかりづらいコードで、問題を回避できます。先ほどと同じように、for 文のi=1 の時に何が起きているか見てみたいと思います。i=1 の時には、「h1 のonclick イベントに、function(i) { // [function を返すコード] }(i) の実行結果(戻り値が関数)を割り当てろ」という命令がなされています。具体的に言うと、このfunction(i){ }(i) はこのタイミングで直ちに実行されますので、戻ってくる関数は「funcion() { alert(1); }」となります(i=1 なのでalert の中身が1 になる)。つまり、for 文のi=1 の時には、「h1 のonclick イベントにfunction() { alert(1); } を割り当てろ」という命令がなされていることになります。i=1 以外の時も同様で、onclick イベントに割り当てるタイミング(つまりfor 文がまわってるとき)で、その時点でのi を処理してしまっていることが最大のポイントです。その結果、実際にh1 のnode がクリックされると、今度は正しくalert(1) が実行されるようになります。

言葉で書くと返ってわかりづらく見えてしまいましたが、そういうことです。 この、無名関数を定義して、すぐに実行するという、function(){}(); というのは結構便利なので、覚えておくと良いかもしれません。

この記事のトラックバックURL
http://en.yummy.stripper.jp/trackback/1308069
トラックバック
[JavaScript]jQuery のイベントとクロージャ
以前のエントリ([JavaScript]僕、スコープとかクロージャとか曖昧でした )で書いた、Javascript のクロージャの話と全く同じことなのですが、あれをjQuery のclick イベントで書くとどうなるかということを、自分用にメモとして残しておきたいと思います。 例えば、
| ブログが続かないわけ | 2010/02/28 11:58 AM |
コメント
<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<h1>Java Script</h1>
<h2>The Good Parts</h2>
<pre>text</pre>
</body>
<script type="text/javascript">
var nodes = document.body.childNodes;

window.onload = function(){
for( var i = 0, len = nodes.length; i < len; i++ ){
nodes[i].onclick = function(n){
// i だとまぎらわしいので 引数を n にしてみました
return function(){
alert(n);
};
}(i);
}
};
</script>
</html>
ロード時に function(n){ CODE }(i); が実行されます。
(i)がついてなければ、関数を定義しているだけになる。
このときに CODE の部分 function(){ alert(n); } を定義してるんですが、n はロード時にセットされている i が、保持されている。

... 説明がひどくてすみません
| Javascriptむずかしい | 2009/10/19 7:13 PM |
いえいえ、コメントありがとうございます。

下から3行目の
}(i);
ここも、
}(n);
ですよね。

僕が書いたものと、同じやりかたですね。
本には、あえて i を使って、ここのi とvar i は違うものだということを示したかったようです。

補足ありがとうございます。
| junichiro | 2009/10/19 7:30 PM |









関連情報