スコープ
変数や関数が参照可能な範囲を定義するプログラミングの基本概念
スコープとは
スコープ (Scope) は、変数や関数が参照可能な範囲を定義するプログラミングの基本概念である。JavaScript のスコープは静的 (レキシカル) に決まる。どの宣言を参照するかはソースコード上のネスト位置だけで確定し、実行時にどこから呼ばれたかには左右されない。だからコードを読むだけで参照先を追える。
範囲を作る単位はグローバル、関数、ブロックの 3 つで、どれに属するかは宣言に使ったキーワードと宣言を置いた場所で決まる。
スコープの種類
| スコープ | 宣言 | 範囲 |
|---|---|---|
| グローバル | 従来のスクリプトのトップレベルの var, function | どこからでも参照可能 (globalThis のプロパティになる) |
| モジュール | ES モジュールのトップレベルの全宣言 | そのファイル内のみ |
| 関数 | var, function | 宣言を囲む最も内側の関数の全体 |
| ブロック | let, const, class | 宣言を囲む {} の中のみ |
トップレベルの宣言が必ずグローバルになるわけではない。<script> で読み込む従来のスクリプトなら var g = 1 は globalThis.g になるが、ES モジュール (type="module" や .mjs) では同じ宣言がモジュールスコープに閉じ、globalThis.g は undefined のままになる。Node.js の CommonJS も各ファイルが関数で包まれるため、同じくグローバルを汚さない。
var と let と const の差
3 つの差はスコープの単位と再代入の可否に集約される。var はブロックを無視して関数全体に広がるため、if や for の中で宣言した変数がブロックの外でも生き残る。let と const はブロック単位で閉じる。
// var: 関数スコープ (ブロックを無視)
if (true) { var x = 1; }
console.log(x); // 1 (ブロックの外でも参照可能)
// let: ブロックスコープ
if (true) { let y = 2; }
console.log(y); // ReferenceError (ブロックの外では参照不可)
// const: ブロックスコープ + 再代入不可
const z = 3;
z = 4; // TypeError (TypeScript ならコンパイル時に検出される)
const が禁じるのは束縛先の差し替えだけで、値の中身を凍結するものではない。const arr = [] に対する arr.push(1) は通り、arr = [] だけが失敗する。
ホイスティングと一時的デッドゾーン
宣言はスコープの先頭で登録される (ホイスティング) が、登録と初期化は別の段階である。var は登録の時点で undefined が入るため、宣言行より前に読んでもエラーにならず undefined が返る。let と const は登録されるだけで未初期化のまま置かれ、宣言行に到達するまでの区間はアクセスすると例外になる。この区間を一時的デッドゾーン (Temporal Dead Zone) と呼ぶ。
function f() {
console.log(v); // undefined (var は登録済み・未代入)
var v = 1;
}
function g() {
console.log(t); // ReferenceError: Cannot access 't' before initialization
let t = 1;
}
デッドゾーン中の変数は typeof でも例外になる点が実務では効く。宣言自体が無い名前に対する typeof x は例外を出さず 'undefined' を返すため、typeof で存在を確かめる書き方は let と const には通じない。宣言してから使う順序を守るのが唯一の回避策である。
レキシカルスコープの解決順
変数名は、宣言を囲むスコープを内側から外側へ順にたどって解決される。最初に見つかった宣言が参照先になり、最も外までたどって見つからなければ ReferenceError になる。参照は内側から外側への一方通行で、外側から内側は見えない。
const outer = 'outer';
function foo() {
const inner = 'inner';
console.log(outer); // ✅ 外側のスコープを参照可能
}
console.log(inner); // ❌ ReferenceError (関数の内側は外から見えない)
たどる経路は定義位置で固定される。下の show は caller から呼ばれても caller の label ではなく、自分が定義された位置から見える label を返す。呼び出し元のスコープを参照する方式 (動的スコープ) を採る言語とは、ここが分かれる。
const label = 'module';
function show() { return label; }
function caller() {
const label = 'caller';
return show(); // 'module' が返る
}
クロージャとスコープ
関数が定義位置のスコープを抱えたまま外へ持ち出される形がクロージャである。外側の関数が終了してもそのスコープは解放されず、返された関数からだけ参照できる状態として残る。
function createCounter() {
let count = 0; // createCounter のスコープ
return () => ++count; // 定義位置のスコープを抱えて外へ出る
}
const counter = createCounter();
counter(); // 1
counter(); // 2
// count は外部からアクセスできないが、返された関数からは参照可能
サーバーレスではスコープが寿命を決める
スコープの寿命が実行環境の挙動に直結する場面もある。AWS Lambda では、ハンドラの外側 (モジュールスコープ) に書いた初期化はコールドスタート時に 1 回だけ走り、同じ実行環境が再利用される間は残り続ける。ハンドラの内側で宣言した変数は呼び出しごとに作り直される。
| スコープ | Lambda での挙動 |
|---|---|
| モジュールスコープ | コールドスタート時に初期化、ウォームスタートで再利用 |
| ハンドラスコープ | 毎回のリクエストで新規作成 |
この差を利用してコネクションプールや設定の読み込みをモジュールスコープへ置くと、初期化のコストを削れる。反面、リクエスト固有の値をモジュールスコープの変数に持たせると、後続の呼び出しに前のデータが漏れる。モジュールスコープへ置くのは、呼び出しをまたいで共有しても安全なものだけに限る。
スコープの落とし穴
var を使った for は、反復をまたいで 1 個の変数を共有する。setTimeout のコールバックが動くのはループが終わった後なので、3 つのコールバックが同じ変数を読み、終了時点の値だけが見える。let の for は反復ごとに新しい束縛を作るため、各コールバックが自分の反復の値を持つ。
// ❌ var + ループ: 全て同じ i を参照
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3, 3, 3
}
// ✅ let: 各反復で新しいスコープ
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2
}
ベストプラクティス
宣言のキーワードは、スコープを狭く保つ方向へ倒して選ぶ。
| プラクティス | 理由 |
|---|---|
const をデフォルトで使う | 再代入を防止 |
let は再代入が必要な場合のみ | スコープが明確 |
var は使わない | 関数スコープで予期しない挙動 |
| グローバル変数を避ける | 名前の衝突、テスト困難 |
詳しくは関連書籍を参照。
この記事は役に立ちましたか?
関連用語
関連する記事
ディープラーニング本の選び方 - 原理 / 理論 / 実装 / 数学の 4 系統で整理 (2026 年 8 月時点)
ディープラーニング本の選び方を 4 系統 (原理を手で理解する本 / 理論を体系的に学ぶ本 / フレームワーク実装本 / 数学を補う本) に整理。2026 年 8 月時点の定番書を目的別 / レベル別に紹介し、賞味期限の見極め方も解説します。
DevOps 本ガイド - CI/CD とインフラ自動化を学ぶ技術書の選び方
DevOps の文化と原則から CI/CD、IaC、オブザーバビリティまで学べる技術書の選び方と学習順序を紹介します。
コードを「書く力」と「読む力」は別物 - 読解力を鍛える技術書の使い方
プログラミングの「書く力」ばかり鍛えていませんか。他人のコードを正確に読み解く力は、技術書を使って意識的に鍛えられます。読解力を高める具体的な方法を紹介。