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

私の歴史と今

読んでて恥ずかしくなるのが私の歴史。だけどそのときは今現在のように真面目に書いていた訳でね。そんな私の今を書いていく。

スコープで曖昧だったこと

関数スコープは、動的ではなく、静的であるということ。スコープチェーンは定義時に決まり、実行時には決まらない。それを認識できていなかったんだけど、わかりにくくしている原因は、「this」だった。thisが参照するオブジェクトは、動的に、実行時に決まる。関数定義時には決まっていない。下記のように、「this」が参照するオブジェクトは、(1)と(2)の時で異なる。

function f(){
  return function(){
    alert(this.x);
  };
}
var o1 = {x:'a', c:f()};
var o2 = {x:'b', c:f()};
o1.c(); // (1)…「a」と表示される。thisが参照するオブジェクトはo1と同じ。
o2.c(); // (2)…「b」と表示される。thisが参照するオブジェクトはo2と同じ。

thisは実行時に(=動的に)決まる。だけど、スコープチェーンは定義時に(=静的に)決まる。
簡単なクロージャの例。alert(y)を本体に持つクロージャは、定義時にスコープチェーンが設定される。設定されるスコープチェーンは、「関数fのCallオブジェクト」→「グローバルオブジェクト」となっているはず。だから、実行時の変数yの名前解決は、クロージャ内に変数yが宣言されていないから、スコープチェーンの次に位置する関数fのCallオブジェクトのプロパティy(すなわち関数fの引数y)によって解決できる。

function f(y){
  return function(){
    alert(y);
  };  // この時点で内側の関数(クロージャ)が定義される。
      // それと同時に、この関数にスコープチェーンが設定される。
      // そのスコープチェーンは、「関数fのCallオブジェクト」→「グローバルオブジェクト」。
      // 関数fのCallオブジェクトは、内側関数の定義時、つまり関数fの実行時に決まる。
}
f(1)() // (1)…「1」と表示される。
f(2)() // (2)…「2」と表示される。
       // (1)と(2)の関数fのスコープは別物。つまり、クロージャを定義した時のスコープが別物。

でも、スコープチェーンが静的に決まることを認識していなかったので、下記のようなことができるのではないか?と考えてしまった。

var f1 = function(){
  alert(x);
}
var f2 = function(fn){
  var x = 0;
  fn();  // 内部処理は「alert(x)」
}
f2(f1);  // 「0」と表示されると思ってしまった…。けど「x is not found」

スコープチェーンが実行時に決まるのなら、f2(f1)が呼び出され、更にf2内部でfn()が呼び出された時、fn関数内の変数xは、f2関数内で宣言された変数xによって解決されるはず。だけど、解決できないということは、静的に決まるということ。
わかってしまえば当たり前すぎることかもしれない。JavaScriptのコールバック関数のイメージが強すぎた。「this」のイメージも強烈だったのかもしれない。関数を引数として渡せばオブジェクト内にアクセス可能だということを、this(変数じゃないけど)以外の変数にも拡大解釈してしまったのが間違いだった。
だけど、間違ってはいけないのは、スコープチェーンは関数定義時に固定されるけど、その関数のCallオブジェクトがスコープチェーンに追加されるのは実行時だということ。また、スコープチェーンの各プロパティも実行時に決まること。たとえば、Argumentsオブジェクトや名前付きの引数。