[JavaScript講座] スコープと実行コンテキスト

当ページのリンクには広告が含まれています。
目次

スコープの整理とレキシカルスコープ

スコープとは何か

スコープは一言で言うと、「その変数・関数がどこから見えるか」という“有効範囲”です。

代表的には 3 種類:

  1. グローバルスコープ
    • ファイル全体(またはスクリプト全体)で共有される領域
  2. 関数スコープ
    • function の中だけ有効な領域
  3. ブロックスコープ
    • { ... } の中で let / const で宣言されたものの領域
const globalVar = "global"; // グローバル

function func() {           // 関数スコープ
  const inFunc = "func";

  if (true) {               // ブロックスコープ
    const inBlock = "block";
    console.log(globalVar); // OK
    console.log(inFunc);    // OK
    console.log(inBlock);   // OK
  }

  // console.log(inBlock);  // NG(ブロック外なのでエラー)
}

func();

// console.log(inFunc);    // NG(関数外なのでエラー)

ルールの直感:

  • 内側から外側は見える
  • 外側から内側は見えない

という「入れ子構造」になっています。

レキシカルスコープ(静的スコープ)

JavaScript は「レキシカルスコープ」言語です。

レキシカルスコープとは、

どの変数にアクセスできるかは、「どこに書いたか(定義位置)」で決まる

というルールです。「どこから呼ばれたか」ではありません。

const value = "global";

function printValue() {
  console.log(value);
}

function run() {
  const value = "local";
  printValue();
}

run(); // "global"
  • printValue は 定義された場所(グローバル)から外側へ変数を探す
  • run の中で呼んでいても、runvalue は見ない
    → 「どこで呼ぶか」ではなく「どこに書いてあるか」が効く

この「定義位置ベース」という性質が、クロージャや this の理解にも絡んできます。

実行コンテキストとは何か

実行コンテキストのイメージ

実行コンテキスト(Execution Context)は、砕いて言うと:

「いまこのコードを実行するために必要な情報セット」

です。1つのコンテキストの中に、ざっくり次のようなものが含まれます:

  • そのスコープで使える 変数・関数の環境
  • 現在の this が何か
  • 外側スコープへの参照(スコープチェーン)

実行コンテキストの種類

主に 3 種類あります:

  1. グローバル実行コンテキスト
    • プログラム開始時に 1 つだけ作られる
    • グローバル変数・グローバル関数・グローバル this など
  2. 関数実行コンテキスト
    • 関数が呼ばれるたびに新しく作られる
    • その関数のローカル変数・引数・内部関数・this など
  3. eval 実行コンテキスト
    • eval() を使ったときに作られる(通常はほぼ使わないので、イメージだけでOK)

実務的には、グローバル と 関数 の 2 つが分かっていれば困りません。

実行コンテキストは「生成フェーズ」と「実行フェーズ」がある

JavaScript エンジンは、1つの実行コンテキストに対して

  1. 生成フェーズ(Creation Phase)
  2. 実行フェーズ(Execution Phase)

の 2 ステップで処理します。

生成フェーズで何が行われるか

ざっくりいうと、「実行前の仕込み」です。

  • var 宣言や関数宣言をスキャンして登録
    • var 変数名を登録して、値を undefined にしておく
    • 関数宣言は 本体ごと丸ごと登録(すぐ呼べる状態になる)
  • let / const も名前だけ登録するが、まだ「使用不可の状態(TDZ)」
    → 初期化前に触ると ReferenceError

この段階が、いわゆる Hoisting(巻き上げ) の正体です。

実行フェーズ

その後、コードを上から順番に実行していきます:

  • 実際に代入が行われる
  • 関数が呼ばれる
  • 条件分岐・ループなどを解決

具体例:

console.log(x); // ①
console.log(y); // ②

var x = 1;
let y = 2;

生成フェーズでは:

  • var xx を登録して undefined で初期化
  • let yy を登録(TDZ状態)

実行フェーズでは:

  1. console.log(x);xundefinedundefined が出力
  2. console.log(y);y は TDZ 中 → ReferenceError
  3. ② でエラーになるので、その下は実行されない

varlet / const の違いが、「実行コンテキスト生成フェーズの挙動の違い」と結びつきます。

コールスタックと関数実行コンテキスト

コールスタックとは

「どの関数が、どの順番でネストして実行されているか」

を管理する「スタック(後入れ先出し)」です。

function f1() {
  console.log("f1 start");
  f2();
  console.log("f1 end");
}

function f2() {
  console.log("f2 start");
  f3();
  console.log("f2 end");
}

function f3() {
  console.log("f3");
}

f1();

おおまかな流れ:

  1. プログラム開始 → グローバル実行コンテキストがスタックに乗る
  2. f1() 呼び出し → f1 の実行コンテキストを作成 → スタックに push
  3. f2() 呼び出し → f2 の実行コンテキストを push
  4. f3() 呼び出し → f3 の実行コンテキストを push
  5. f3 終了 → f3 のコンテキストを pop
  6. f2 に戻り、残りを実行 → 終了 → f2 のコンテキストを pop
  7. f1 に戻り、残りを実行 → 終了 → f1 のコンテキストを pop
  8. 最後にグローバルコンテキストだけが残る

ポイント:

  • 関数が呼ばれるたびに 新しい関数実行コンテキスト が作られ、コールスタックに積まれる
  • 関数が終了すると、その実行コンテキストはコールスタックから外れ、
    参照されていないローカル変数は GC の対象になる(※クロージャが掴んでいれば残る)

実行コンテキストとスコープチェーン

スコープチェーンのイメージ

実行コンテキストは「いまの関数の変数環境」だけでなく、
外側のスコープへの参照も持っています。

const a = 1; // グローバル

function outer() {
  const b = 2;

  function inner() {
    const c = 3;
    console.log(a, b, c);
  }

  inner();
}

outer(); // 1 2 3

inner 実行時のスコープチェーン(変数探索の順番)は:

  1. inner のローカル(c
  2. outer のローカル(b
  3. グローバル(a

という鎖になっています。

変数解決の流れ:

  • c → inner 内で見つかる
  • b → inner にはないので outer で見つかる
  • a → outer にもないのでグローバルで見つかる

この「内側から外側へ向かってたどる鎖」が、スコープチェーンです。

strict / 非strict における this の違い(関数内部)

ここでは「実行コンテキスト+strictモード」の観点から this の違いを押さえます。

非strictモードでの関数内 this

// 非strict モード
function showThis() {
  console.log(this);
}

showThis(); // ブラウザ: window, Node: グローバルオブジェクト
  • 単純な関数呼び出し showThis() の場合、
    非strictモードでは this は グローバルオブジェクト になります。

また、こんなパターンも危険です:

function foo() {
  // 「暗黙のグローバル」になる悪い例(非strict)
  bar = 123;
}

foo();
console.log(bar); // 123(window.bar)
  • 非strict だと、var / let / const なしで代入すると グローバル変数が勝手に作られる
  • this がグローバルであることと合わせて、非常にバグの温床になります

strictモードでの関数内 this

"use strict";

function showThis() {
  console.log(this);
}

showThis(); // undefined
  • strictモードでは、単純な関数呼び出しにおける thisundefined になります。
  • さらに、暗黙のグローバルも禁止されます:
"use strict";

function foo() {
  bar = 123; // ReferenceError: bar is not defined
}

foo();

まとめると:

  • 非strict:
    • 単純呼び出しの this → グローバルオブジェクト
    • 暗黙のグローバルが許される
  • strict:
    • 単純呼び出しの thisundefined
    • 暗黙のグローバルは禁止(ReferenceError)

この違いは、「実行コンテキストの this バインディングの初期値が違う」と考えると整理できます。

実行コンテキストとクロージャへの橋渡し

実行コンテキストとどう関係しているかを、軽く解説します。

function createCounter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const counter = createCounter();

counter(); // 1
counter(); // 2

このコードでは:

  1. createCounter() 呼び出しで、「createCounter 実行コンテキスト」が作られる
  2. その中に count というローカル変数がある
  3. 戻り値として 内部関数(無名関数)が返される
  4. createCounter の実行自体は終わるが、
    戻り値の関数が count を参照し続けているため、count を含む環境だけが生き残る

ここで「外側の実行コンテキストの一部(環境)を抱えた関数」がクロージャです。

実行コンテキストが「その場限りで消えず、一部が延命される」
→ それを抱えた関数 = クロージャ

というイメージを持っておくと、クロージャ理解がかなりスムーズになります。


<<前へ(アロー関数(Arrow Function))

>>次へ(this 完全理解とクロージャ)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次