読者です 読者をやめる 読者になる 読者になる

JavaScriptの変数スコープと(軽く)クロージャ。

JavaScriptの変数スコープには2つしかありません。

  • グローバルスコープ
  • 関数スコープ

サンプルコードで動作を確認していきます。

サンプルコード

Sample01

コード
(function(){
  scope = 'Global Scope'; 
}());
(function(){
  console.log('@Sample01');
  console.log(scope);
}());
実行結果
@Sample01
Global Scope 
解説

JavaScriptでは var を付けづに変数を宣言するとグローバル変数として宣言されます。
グローバル変数として宣言された変数は他の関数からも参照できる事がわかります。

Sample02

コード
(function(){
  var scope = 'Global Scope';
  (function(){
    console.log('@Sample02');
    console.log(scope);
  }());
}());
実行結果
@Sample02
Global Scope 
解説

このサンプルでは実行される関数の呼び出し元で宣言された変数も参照できる事がわかります。

Sample03

コード
(function(){
  var scope = 'Global Scope';
  (function(){
    console.log('@Sample03-1');
    var scope = 'Function Scope';
    console.log(scope);
  }());

  (function(){
    console.log('@Sample03-2');
    console.log(scope);
  }());
}());
実行結果
@Sample03-1
Function Scope
@Sample03-2
Global Scope 
解説

このサンプルでは実行される関数の中で、呼び出し元で宣言された変数と同じ名前の変数を宣言したとしても、呼び出し元で宣言された変数とは別の変数を定義していることがわかります。

Sample04

コード
var scope = 'Global Scope'; 
(function(){
  console.log('@Sample04');
  console.log(scope);
  var scope = 'Function Scope';
  console.log(scope);
}());
実行結果
@Sample04
undefined
Function Scope 
解説

1回目に scope を出力しているところで 'GlocalScope' と出力されそうですが、 undefiend が出力されています。
これは、関数オブジェクトの中で scope 属性がホイストされているからです。
ホイスト後のコードは下記のようになります。

var scope = 'Global Scope'; 
(function(){
  var scope;
  console.log('@Sample04');
  console.log(scope);
  scope = 'Function Scope';
  console.log(scope);
}());

Sample05

コード
(function(){
  console.log('@Sample05');  
  for(var i=0; i<3; i++){
    console.log('[In for]i is ' + i); 
  }
  console.log('[Out for]i is ' + i);   
}());
実行結果
@Sample05
[In for]i is 0
[In for]i is 1
[In for]i is 2
[Out for]i is 3 
解説

JavaScriptではブロックスコープがないためfor文で宣言した変数もfor文の外から参照できることがわかります。

Sample06

コード
function mockButton(){ return { onclick : {} }; }
(function(){
  console.log('@Sample06');
  var buttons = [mockButton(),mockButton(),mockButton()];

  for(var i=0, l=buttons.length; i<l; i++){
    buttons[i].onclick = function(){
      console.log('onclick' + i);
    };
  }

  for(var j=0, l=buttons.length; j<l; j++){
    buttons[j].onclick();
  }
}());
実行結果
@Sample06
onclick3
onclick3
onclick3 
解説

onclick0 onclick1 onclick2 と出力されることを期待しましたが onclick3 が3回出力されてしまいました。
2つ目のfor文のところでも i は有効なため、 i の最後に設定された値 3 が出力されています。
これを回避するためにクロージャを使い実装します。Sample07です。

Sample07

コード
function mockButton(){ return { onclick : {} }; }
(function(){
  console.log('@Sample07');
  var buttons = [mockButton(),mockButton(),mockButton()];

  for(var i=0, l=buttons.length; i<l; i++){
    (function(i){
      buttons[i].onclick = function(){
        console.log('onclick' + i);
      };
    }(i));
  }

  for(var j=0, l=buttons.length; j<l; j++){
    buttons[j].onclick();
  }
}());
実行結果
@Sample07
onclick0
onclick1
onclick2 
解説

期待通りの動きになりました。
onclickに関数を設定している無名関数で外部より参照されない i を受け取っているのでonclickに関数を設定している時点で i が定義されています。

Sample08

コード
function mockButton(){ return { onclick : {} }; }
(function(){
  console.log('@Sample08');
  var buttons = [mockButton(),mockButton(),mockButton()];

  for(var i=0, l=buttons.length; i<l; i++){
    (function(){
      buttons[i].onclick = function(){
        console.log('onclick' + i);
      };
    }(i));
  }

  for(var j=0, l=buttons.length; j<l; j++){
    buttons[j].onclick();
  }
}());
実行結果
@Sample08
onclick3
onclick3
onclick3 
解説

冗長な解説になってしまいますが、このようにonclickに関数を i を引数として与えないとonclick関数が外部にある i を参照してしまい、期待した結果になりません。

Sample08

コード
function mockButton(){ return { onclick : {} }; }
(function(){
  console.log('@Sample08');
  var buttons = [mockButton(),mockButton(),mockButton()];

  for(var i=0, l=buttons.length; i<l; i++){
    (function(){
      buttons[i].onclick = function(){
        console.log('onclick' + i);
      };
    }(i));
  }

  for(var j=0, l=buttons.length; j<l; j++){
    buttons[j].onclick();
  }
}());
実行結果
@Sample08
onclick3
onclick3
onclick3 
解説

冗長な解説になってしまいますが、このようにonclickに関数を i を引数として与えないとonclick関数が外部にある i を参照してしまい、期待した結果になりません。

サンプルコード(補足)

上記の内容で十分説明できているので下記のコードの紹介は不要だと思いますが、混乱しやすい内容なので記載しておきます。

Sample09

コード
function mockButton(){ return { onclick : {} }; }
(function(){
  console.log('@Sample09');
  var buttons = [mockButton(),mockButton(),mockButton()];

  for(var i=0, l=buttons.length; i<l; i++){
    buttons[i].onclick = function(){
      console.log('onclick' + i);
    };
  }

  for(var i=0, l=buttons.length; i<l; i++){
    buttons[i].onclick();
  }
}());
実行結果
@Sample09
onclick0
onclick1
onclick2 
解説

期待した通りに動いているように見えますが、これは2つ目のfor文で i を上書きしているのでそのように見えるだけです。

Sample10

コード
function mockButton(){ return { onclick : {} }; }
(function(){
  console.log('@Sample10');
  var buttons = [mockButton(),mockButton(),mockButton()];
  
  function clickAll(){
    for(var i=0, l = buttons.length; i<l; i++){
      buttons[i].onclick();
    }
  };
  
  for(var i=0, l = buttons.length; i < l; i++){
    buttons[i].onclick = function(){
      console.log('onclick' + i);
    };
  }
  clickAll();
}());
実行結果
@Sample10
onclick3
onclick3
onclick3 
解説

このサンプルでは clickAll関数 で定義した i が呼び出し元の i と別の変数として定義され、外側の変数に影響を与えていないことがわかります。

最後に。

以上です。
指摘等あればコメント頂けると幸いです。

参考

書籍

テスト駆動JavaScript

テスト駆動JavaScript