LoginSignup
8
11

More than 5 years have passed since last update.

「clickイベントの定義で、変数が想定と違う値で動作する問題」からゆる~く学ぶスコープチェインとクロージャ

Posted at

Javascriptで、ボタンアクションを付与するとき、値が思ったように扱えないところでハマってしまいました。普段他の言語を使っている都合でよくわかっていなかったのですが、調べてみるとJavascriptの スコープチェーン, クロージャという仕様に関わるようでした。最初はちんぷんかんぷんでしたが、なんとなく掴めたような気持ちになったのでまとめてみました。

どんな問題か

idがbutton0,button1,.......,button4をクリックしたときにボタンの番号をアラート表示させたいときのことです。各ボタンに登録されるイベント毎に値が保持されるだろうと次のように記述しました。
(簡易化のためjQueryを使用しています。)

テスト用html

index.html
<html>
    <head>
        <title>ボタンテスト</title>
    </head>
    <body>
        <button id="button0">ボタン0</button>
        <button id="button1">ボタン1</button>
        <button id="button2">ボタン2</button>
        <button id="button3">ボタン3</button>
        <button id="button4">ボタン4</button>

        <script type="text/javascript" src="jquery.min.js"></script>
        <script type="text/javascript" src="script.js"></script>
    </body>
</html>

わからないながらに頑張って書いたJS

script.js(あかんやつ)
for(i=0; i<5; i++){
    $("#button"+i).click(function() {
        alert(i);
    });
}

実行してみる

これを実行してみましょう。
まずボタン0を押してみると...

スクリーンショット 2015-10-14 15.13.23.JPG

なんでやねん!そこは0やろ!
気をとりなおしてボタン1を押してみると...

スクリーンショット 2015-10-14 15.13.23.JPG

Oh...
これは偶然ではないと悟り、原因を探ることにしました。

分析編

スコープチェーン

簡単に言えばスコープが連結してネストされているようなイメージです。ここでおさえておきたいキーワードは グローバルオブジェクトCallオブジェクトです。

グローバルオブジェクトとは

スクリプトが実行される際に内部的に生成されるオブジェクトのこと。グローバル変数やグローバル関数を管理するための便宜的なオブジェクトで、コーディングする我々には見えません。グローバルオブジェクトは変数や関数をプロパティとして管理します。つまりグローバル変数を定義するということはグローバルオブジェクトのプロパティを定義することにほかなりません。

Callオブジェクトとは

Activationオブジェクト、とも言います。Callオブジェクトは関数呼び出しの都度、内部的に生成されるオブジェクトのことです。グローバルオブジェクト同様こちらも、ローカル変数を管理するための便宜的なオブジェクトです。

つまりどういうことだってばよ

スコープチェインはグローバルオブジェクトとCallオブジェクトを生成順に連結したリストです。重複する名前の変数や関数を呼び出す際には内部(後に定義されているもの)から呼び出されます。

図に起こすとこんな感じです。各オブジェクトのプロパティとして変数や関数を保持していると思っていただければと思います。

scopechain.png

先ほどの例ではクリックイベントに登録されている無名関数のCallオブジェクトにはiという変数は存在しなかったので、for文の初期化で定義されているグローバルオブジェクトのプロパティ、つまりグローバル変数iを参照することになります。いずれかのボタンをクリックするときには、すべてのボタンにイベントを割り振っているはずです。すなわちiはループを抜ける時の値、 5になっているのです。うーん、むずかしい。他言語の仕様者にとっては関数単位でオブジェクトが管理されているというのに違和感を感じるでしょう。

解決編

スコープチェインの仕様を踏まえて

先程の仕様を学んだことで、同じ変数iをすべてのボタンで参照しているのがわかりました。頭の良い方はここで一つ解決方法を思いつくはずです。そうです、 繰り返し呼び出されるイベント登録メソッドを関数でくくってしまえば良いのです。このくくるための関数には名前がついています。それが クロージャです。

クロージャ

クロージャはローカル変数を保持するため、関数内にネストされた関数です。普通の関数で使われるローカル変数は関数の処理が終わった段階で破棄されてしまします。しかし内側の関数(クロージャ関数)がローカル変数を参照しているため破棄するわけにはいかなくなります。つまり値を保持することになるのです。

クロージャ関数の作り方

クロージャについてだらだら説明しましたが、要するにどうやって作るんだよ!って話ですよね。作り方は以下のとおりです。

  1. 関数内に関数を作成
  2. 外側の関数で変数を定義
  3. 内側の関数から2.で定義した変数を参照

改善後のスクリプト

以上を踏まえて、先ほどのコードを書きなおしてみましょう。
これで上手く動作するはずです。

script.js(想定通りのやつ)
for(i=0; i<5; i++){
    (function() {
        var num = i;
        $("#button"+num).click(function() {
            alert(num);
        });
    })();
}

実行してみる

さっそく実行してみましょう。
まずボタン0を押してみると...わくわく...
スクリーンショット 2015-10-14 16.24.20.JPG

やった!!5じゃない!!0がでた!!
でも偶然という可能性を考えてボタン1も押してみましょう。

スクリーンショット 2015-10-14 16.24.29.JPG

これは上手くいっていますね!

まとめ

今回は以下の話題を取り上げました。

  • スコープチェイン
  • クロージャ

私自身勉強半ばなので正しいことを書けているかはわかりません。調べたりして、個人的にこうだと思ったことをまとめてみました。もっときっちり勉強したいという方はぐぐってみたり、参考書を読んだりしてみてください!

参考資料

8
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
11