27
Help us understand the problem. What are the problem?

posted at

updated at

【図解】コールスタックとクロージャーを理解する

スコープ

ある変数のスコープとは、その変数にアクセスできるコード上の範囲を意味する。

var で宣言された変数は function scope であるのに対し、letconst で宣言された変数は block scope と呼ばれるスコープを持っている。

function scope の変数は、自分を含む一番内側の関数がスコープとなる。一方 block scope の変数は、自分が属する一番内側の関数orブロックがスコープとなる。ブロックとは { 処理;処理;... } のことである。

var let scope

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 の値(関数)は入っている。

context1.png

1行目の右辺が計算され、x に代入される。

context2.png

if の条件式が true なのでブロックの中に入り、新しい LE が作られる

context3.png

y の値が計算される。

context4.png

次に、f 関数が呼ばれる。関数の中に入るときも LE が作られる

緑の LE が 青の LE ではなく赤の LE につながっているが、これは関数 f の外側のスコープが赤の LE だからである。

実引数として渡した y の値 2 が、f の仮引数 x の値となる。

context5.png

x * 2 を計算するときに x が参照されるが、このとき赤の x ではなく緑の x の値が使われることになる。どのように x を見つけているかというと、Call Stack の先頭(図では一番下)の LE からスタートして、矢印でつながったチェーンをたどって一番最初に見つかった変数が選ばれるのである。そのため、緑の LE をまず探して、そこになければ赤の LE を探すことになるが、今の場合緑の LE に x があるのでそれが選ばれる。変数のシャドーイングはこのようにして発生するのである。

よって x の値は 2 なので 4return される。

context6.png

関数を抜け、緑の LE は Execution Stack から削除される。

戻り値 4z に代入される。

context7.png

ブロックを抜け、赤の LE は Execution Stack から削除され、このまま実行が終了する。

context8.png

クロージャー

クロージャーとは外側のスコープに存在する変数を参照する関数のことである。例えば下のコードでは 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 関数を呼び出すたびに、別々のカウンターが作成されていることが分かるだろう。つまり、c1c2 は別々の変数 x を持っている。これも、LE で説明することができる。

c1c2 が代入された時点では、LE と Call Stack は次のようになっている。c1c2 はどちらも increment 関数の関数オブジェクトだが、別々の LE(緑) を指していることが分かる。これは、関数が呼び出されるたびに LE が作られるからである。

closure1

c1 が実行されると、その関数オブジェクトが指している LE (左の緑の LE) の下に新しい LE が作られ、そこで increment 関数が実行される。

closure2.png

ここで x += 1 が実行されるわけだが、最初の例で説明したとおり、当然この x は左の緑の LE の x を指すことになる。これにより左の緑の LE の x1 になり、右の緑の LE は影響を受けない。つまり、独立した2つの状態変数となっている。

ところで、この2つの x は対応する increment 関数からしかアクセスできない、いわば「隠れた変数」となっている。最近になって JavaScript にプライベートメンバー変数の構文が追加されたが、それまではプライベート変数はクロージャーでしか実現できなかった。


  1. 仕様では Execution Context Stack に詰め込まれるのは Execution Context であるが、ここでは Execution Context と Lexical Environment を同一視している。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
27
Help us understand the problem. What are the problem?