スコープの整理とレキシカルスコープ
スコープとは何か
スコープは一言で言うと、「その変数・関数がどこから見えるか」という“有効範囲”です。
代表的には 3 種類:
- グローバルスコープ
- ファイル全体(またはスクリプト全体)で共有される領域
- 関数スコープ
functionの中だけ有効な領域
- ブロックスコープ
{ ... }の中で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の中で呼んでいても、runのvalueは見ない
→ 「どこで呼ぶか」ではなく「どこに書いてあるか」が効く
この「定義位置ベース」という性質が、クロージャや this の理解にも絡んできます。
実行コンテキストとは何か
実行コンテキストのイメージ
実行コンテキスト(Execution Context)は、砕いて言うと:
「いまこのコードを実行するために必要な情報セット」
です。1つのコンテキストの中に、ざっくり次のようなものが含まれます:
- そのスコープで使える 変数・関数の環境
- 現在の
thisが何か - 外側スコープへの参照(スコープチェーン)
実行コンテキストの種類
主に 3 種類あります:
- グローバル実行コンテキスト
- プログラム開始時に 1 つだけ作られる
- グローバル変数・グローバル関数・グローバル
thisなど
- 関数実行コンテキスト
- 関数が呼ばれるたびに新しく作られる
- その関数のローカル変数・引数・内部関数・
thisなど
- eval 実行コンテキスト
eval()を使ったときに作られる(通常はほぼ使わないので、イメージだけでOK)
実務的には、グローバル と 関数 の 2 つが分かっていれば困りません。
実行コンテキストは「生成フェーズ」と「実行フェーズ」がある
JavaScript エンジンは、1つの実行コンテキストに対して
- 生成フェーズ(Creation Phase)
- 実行フェーズ(Execution Phase)
の 2 ステップで処理します。
生成フェーズで何が行われるか
ざっくりいうと、「実行前の仕込み」です。
var宣言や関数宣言をスキャンして登録var変数名を登録して、値をundefinedにしておく- 関数宣言は 本体ごと丸ごと登録(すぐ呼べる状態になる)
let/constも名前だけ登録するが、まだ「使用不可の状態(TDZ)」
→ 初期化前に触るとReferenceError
この段階が、いわゆる Hoisting(巻き上げ) の正体です。
実行フェーズ
その後、コードを上から順番に実行していきます:
- 実際に代入が行われる
- 関数が呼ばれる
- 条件分岐・ループなどを解決
具体例:
console.log(x); // ①
console.log(y); // ②
var x = 1;
let y = 2;生成フェーズでは:
var x→xを登録してundefinedで初期化let y→yを登録(TDZ状態)
実行フェーズでは:
console.log(x);→xはundefined→undefinedが出力console.log(y);→yは TDZ 中 →ReferenceError- ② でエラーになるので、その下は実行されない
var と let / 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();おおまかな流れ:
- プログラム開始 → グローバル実行コンテキストがスタックに乗る
f1()呼び出し →f1の実行コンテキストを作成 → スタックに pushf2()呼び出し →f2の実行コンテキストを pushf3()呼び出し →f3の実行コンテキストを pushf3終了 →f3のコンテキストを popf2に戻り、残りを実行 → 終了 →f2のコンテキストを popf1に戻り、残りを実行 → 終了 →f1のコンテキストを pop- 最後にグローバルコンテキストだけが残る
ポイント:
- 関数が呼ばれるたびに 新しい関数実行コンテキスト が作られ、コールスタックに積まれる
- 関数が終了すると、その実行コンテキストはコールスタックから外れ、
参照されていないローカル変数は GC の対象になる(※クロージャが掴んでいれば残る)
実行コンテキストとスコープチェーン
スコープチェーンのイメージ
実行コンテキストは「いまの関数の変数環境」だけでなく、
外側のスコープへの参照も持っています。
const a = 1; // グローバル
function outer() {
const b = 2;
function inner() {
const c = 3;
console.log(a, b, c);
}
inner();
}
outer(); // 1 2 3inner 実行時のスコープチェーン(変数探索の順番)は:
- inner のローカル(
c) - outer のローカル(
b) - グローバル(
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モードでは、単純な関数呼び出しにおける
thisはundefinedになります。 - さらに、暗黙のグローバルも禁止されます:
"use strict";
function foo() {
bar = 123; // ReferenceError: bar is not defined
}
foo();まとめると:
- 非strict:
- 単純呼び出しの
this→ グローバルオブジェクト - 暗黙のグローバルが許される
- 単純呼び出しの
- strict:
- 単純呼び出しの
this→undefined - 暗黙のグローバルは禁止(ReferenceError)
- 単純呼び出しの
この違いは、「実行コンテキストの this バインディングの初期値が違う」と考えると整理できます。
実行コンテキストとクロージャへの橋渡し
実行コンテキストとどう関係しているかを、軽く解説します。
function createCounter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2このコードでは:
createCounter()呼び出しで、「createCounter 実行コンテキスト」が作られる- その中に
countというローカル変数がある - 戻り値として 内部関数(無名関数)が返される
createCounterの実行自体は終わるが、
戻り値の関数がcountを参照し続けているため、countを含む環境だけが生き残る
ここで「外側の実行コンテキストの一部(環境)を抱えた関数」がクロージャです。
実行コンテキストが「その場限りで消えず、一部が延命される」
→ それを抱えた関数 = クロージャ
というイメージを持っておくと、クロージャ理解がかなりスムーズになります。
コメント