スコープ
ある変数のスコープとは、その変数にアクセスできるコード上の範囲を意味する。
var
で宣言された変数は function scope であるのに対し、let
や const
で宣言された変数は block scope と呼ばれるスコープを持っている。
function scope の変数は、自分を含む一番内側の関数がスコープとなる。一方 block scope の変数は、自分が属する一番内側の関数orブロックがスコープとなる。ブロックとは { 処理;処理;... }
のことである。
Lexical Environment (LE)
JavaScript の実行環境はプログラムを実行する上で「どの変数が何の値を持っているか」というテーブルのようなものを保持しながらコードを一行一行実行する。ただし、変数はスコープというものがあり、スコープの外からはアクセスすることはできない。また、変数を参照するとき、同じ名前の変数が複数あればより内側のスコープに存在するものが優先される(シャドウイング)。また、後で説明するがJavaScriptの関数はクロージャーという機能を持っている。
このような性質を実現するために、JavaScript ではテーブルのチェーン構造によって変数を管理している。このテーブル一つ一つを Lexical Environment と呼び、変数の名前と値のペアを格納する。
Execution Context Stack
Call Stack とも呼ばれるが、これは「今実行中の関数はどの関数から呼び出されて、その関数はどの関数から呼び出されて...」という履歴のようなものを格納するスタック構造である。そしてこのスタックに詰め込まれるのは Lexical Environment である1。
実行の流れ
例えば以下のコードを考えよう。
const x = 1
if (true) {
const y = x + 1
const z = f(y)
}
function f(x) {
return x * 2
}
まずは、トップレベル(一番外側のスコープ)の変数のための LE が作られる。画像右の白い四角が LE を表す。この時点では通常の変数に値は入っていない(初期化されていない)。しかし、関数宣言は hoisting (巻き上げ) が起こるので、既に変数 f
の値(関数)は入っている。
1行目の右辺が計算され、x
に代入される。
if
の条件式が true
なのでブロックの中に入り、新しい LE が作られる。
y
の値が計算される。
次に、f
関数が呼ばれる。関数の中に入るときも LE が作られる。
緑の LE が 青の LE ではなく赤の LE につながっているが、これは関数 f
の外側のスコープが赤の LE だからである。
実引数として渡した y
の値 2
が、f
の仮引数 x
の値となる。
x * 2
を計算するときに x
が参照されるが、このとき赤の x
ではなく緑の x
の値が使われることになる。どのように x
を見つけているかというと、Call Stack の先頭(図では一番下)の LE からスタートして、矢印でつながったチェーンをたどって一番最初に見つかった変数が選ばれるのである。そのため、緑の LE をまず探して、そこになければ赤の LE を探すことになるが、今の場合緑の LE に x
があるのでそれが選ばれる。変数のシャドーイングはこのようにして発生するのである。
よって x
の値は 2
なので 4
が return
される。
関数を抜け、緑の LE は Execution Stack から削除される。
戻り値 4
が z
に代入される。
ブロックを抜け、赤の LE は Execution Stack から削除され、このまま実行が終了する。
クロージャー
クロージャーとは外側のスコープに存在する変数を参照する関数のことである。例えば下のコードでは increment
関数がクロージャーである。
const c1 = counter()
const c2 = counter()
console.log(c1())
console.log(c1())
console.log(c1())
console.log(c2())
console.log(c2())
function counter() {
let x = 0
function increment() {
x += 1
return x
}
return increment
}
実行すると以下が出力される。
1
2
3
1
2
counter
関数を呼び出すたびに、別々のカウンターが作成されていることが分かるだろう。つまり、c1
と c2
は別々の変数 x
を持っている。これも、LE で説明することができる。
c1
と c2
が代入された時点では、LE と Call Stack は次のようになっている。c1
と c2
はどちらも increment
関数の関数オブジェクトだが、別々の LE(緑) を指していることが分かる。これは、関数が呼び出されるたびに LE が作られるからである。
c1
が実行されると、その関数オブジェクトが指している LE (左の緑の LE) の下に新しい LE が作られ、そこで increment
関数が実行される。
ここで x += 1
が実行されるわけだが、最初の例で説明したとおり、当然この x
は左の緑の LE の x
を指すことになる。これにより左の緑の LE の x
が 1
になり、右の緑の LE は影響を受けない。つまり、独立した2つの状態変数となっている。
ところで、この2つの x
は対応する increment
関数からしかアクセスできない、いわば「隠れた変数」となっている。最近になって JavaScript にプライベートメンバー変数の構文が追加されたが、それまではプライベート変数はクロージャーでしか実現できなかった。
-
仕様では Execution Context Stack に詰め込まれるのは Execution Context であるが、ここでは Execution Context と Lexical Environment を同一視している。 ↩