レキシカルスコープって何者?
レキシカルスコープは、
「変数がどこから見えるかは、“コードが書かれている場所”で決まる」
というルールのことです。
もう少し砕くと、
「どの変数が使えるか」は
「どこでその関数を“定義したか”で決まり、
“どこから呼び出したか”では変わらない。
これがレキシカル(静的)スコープです。
JavaScript は、このレキシカルスコープを採用しています。
まずは「どこで定義したか」が効いてくる例
呼び出し場所が違っても、見える変数は同じ
次のコードを見てください。
const value = "グローバル";
function printValue() {
console.log(value);
}
function run() {
const value = "runの中";
printValue();
}
run(); // 何が出る?
JavaScript直感的に「run の中から呼んでいるから runの中 が出そう」と思うかもしれませんが、
実際の出力はこうです。
グローバル
なぜか?
printValue は「どこで定義されたか」を見ると、
グローバルスコープにあります。
そのときに見えている value は「グローバルの value」です。
run の中で printValue() を呼んでも、printValue が「見ているスコープ」は変わりません。
ここが重要です。
レキシカルスコープでは、「関数がどこで“書かれたか”がすべて。
“どこから呼ばれたか”は関係ない。
「呼び出し元ではなく、定義場所」がスコープを決める
もう少し分かりやすく分解してみる
さっきの例を、少し書き換えてみます。
const value = "外";
function outer() {
const value = "outerの中";
function inner() {
console.log(value);
}
return inner;
}
const fn = outer();
fn(); // 何が出る?
JavaScript出力はこうです。
outerの中
ここで起きていることを言葉で追うと、
- グローバルに
value = "外"がある outerの中でvalue = "outerの中"を定義innerはouterの中で定義されているouter()を呼ぶと、innerが返ってくるfn()としてinnerを呼び出す
このとき、inner が参照する value は
「自分が定義された場所(outer の中)で見えていた value」です。
fn() をグローバルから呼んでいるからといって、
グローバルの value を見るわけではありません。
ここがレキシカルスコープの核心です。
「関数は、自分が“生まれた場所のスコープ”を覚えている」
と言ってもいいです。
レキシカルスコープと「スコープの入れ子」
コードの“見た目の入れ子”が、そのままスコープの入れ子になる
次のコードを見てください。
const a = "A";
function f1() {
const b = "B";
function f2() {
const c = "C";
console.log(a, b, c);
}
f2();
}
f1();
JavaScript出力はこうです。
A B C
ここでのスコープの関係は、
グローバル:a
f1 の中:a, b
f2 の中:a, b, c
という「入れ子」になっています。
ポイントは、
この入れ子構造は「コードの見た目の入れ子」と一致している
ということです。
f2 は f1 の中に「書かれている」ので、f1 のスコープを“外側”として持っています。
これが「レキシカル(書かれた場所ベース)のスコープ」です。
「動的スコープ」との違いをざっくりイメージする
JavaScript は「呼び出し元」ではなく「定義場所」を見る
レキシカルスコープの反対の考え方として、
「動的スコープ」というものがあります(JavaScript では採用していません)。
動的スコープの世界では、
「どこから呼ばれたか」でスコープが決まります。
さっきの例を動的スコープ風に考えると、printValue を run から呼んだときにrun の中の value が見える、という挙動になります。
でも JavaScript はそうではない。
「どこから呼ばれたか」ではなく「どこで書かれたか」で決まる。
この違いを意識しておくと、
「なんでこの変数が見えるんだっけ?」というときに迷いにくくなります。
レキシカルスコープとクロージャの関係
「外側の変数を覚えておく関数」が自然に生まれる
レキシカルスコープを理解すると、
クロージャの仕組みもスッと入ってきます。
さっきの createCounter をもう一度。
function createCounter() {
let count = 0;
function increment() {
count += 1;
console.log(count);
}
return increment;
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
JavaScriptincrement は createCounter の中で定義されています。
だから、レキシカルスコープのルールに従って、
「外側のスコープ(createCounter の中)の count」を参照できます。
createCounter() の呼び出しが終わったあとも、increment は「自分が生まれた場所のスコープ」を覚えているので、count にアクセスし続けられます。
ここが重要です。
クロージャは「レキシカルスコープがある世界で、
“外側の変数を覚えたまま生き続ける関数”」にすぎない。
土台にあるのは常にレキシカルスコープです。
実務でレキシカルスコープを意識する場面
「この関数から見える変数は何か?」を“定義場所”で考える
例えば、イベントハンドラの中で外側の変数を使うとき。
function setup() {
const userName = "太郎";
button.addEventListener("click", () => {
console.log("こんにちは、" + userName + "さん");
});
}
JavaScriptこの () => { ... } は setup の中で定義されています。
だから、userName を参照できます。
setup が呼ばれたあと、
ボタンがクリックされるのはもっと後かもしれません。
それでも userName が見えるのは、
「定義場所のスコープを覚えている」からです。
ここで考えるべきなのは、
「この関数はどこで定義されているか?」
「そのとき、どの変数が見えていたか?」
という視点です。
呼び出しタイミングや呼び出し元ではなく、
「書かれている位置」を基準に考える。
これがレキシカルスコープを使いこなすコツです。
初心者として「レキシカルスコープ」で本当に押さえてほしいこと
レキシカルスコープは、
「スコープは“書かれた場所”で決まる」 というルール。
関数がどこから呼ばれるかではなく、
どこで定義されているかで、
見える変数が決まる。
内側の関数は、
自分が書かれている場所の外側スコープを“覚えている”。
このルールのおかげで、
コードの見た目の入れ子構造と、
スコープの入れ子構造が一致する
(=人間にとって理解しやすい)
クロージャのような「外側の変数を覚えた関数」が自然に作れる
という世界が成り立っています。
コードを読むとき・書くときに、
「この関数は“どこで書かれているか”?
そのとき“何が見えていたか”?」
を意識してみてください。
それができるようになると、
スコープの話は「難しい理論」ではなく、
「当たり前の空気感」として体に馴染んでいきます。
