JavaScript | 基礎構文:スコープ・実行コンテキスト - レキシカルスコープ

JavaScript JavaScript
スポンサーリンク
スポンサーリンク

レキシカルスコープって何者?

レキシカルスコープは、
「変数がどこから見えるかは、“コードが書かれている場所”で決まる」
というルールのことです。

もう少し砕くと、

「どの変数が使えるか」は
「どこでその関数を“定義したか”で決まり、
“どこから呼び出したか”では変わらない。

これがレキシカル(静的)スコープです。
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の中

ここで起きていることを言葉で追うと、

  1. グローバルに value = "外" がある
  2. outer の中で value = "outerの中" を定義
  3. inner は outer の中で定義されている
  4. outer() を呼ぶと、inner が返ってくる
  5. 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
JavaScript

increment は createCounter の中で定義されています。
だから、レキシカルスコープのルールに従って、
「外側のスコープ(createCounter の中)の count」を参照できます。

createCounter() の呼び出しが終わったあとも、
increment は「自分が生まれた場所のスコープ」を覚えているので、
count にアクセスし続けられます。

ここが重要です。
クロージャは「レキシカルスコープがある世界で、
“外側の変数を覚えたまま生き続ける関数”」にすぎない。
土台にあるのは常にレキシカルスコープです。


実務でレキシカルスコープを意識する場面

「この関数から見える変数は何か?」を“定義場所”で考える

例えば、イベントハンドラの中で外側の変数を使うとき。

function setup() {
  const userName = "太郎";

  button.addEventListener("click", () => {
    console.log("こんにちは、" + userName + "さん");
  });
}
JavaScript

この () => { ... } は setup の中で定義されています。
だから、userName を参照できます。

setup が呼ばれたあと、
ボタンがクリックされるのはもっと後かもしれません。
それでも userName が見えるのは、
「定義場所のスコープを覚えている」からです。

ここで考えるべきなのは、
「この関数はどこで定義されているか?」
「そのとき、どの変数が見えていたか?」
という視点です。

呼び出しタイミングや呼び出し元ではなく、
「書かれている位置」を基準に考える。
これがレキシカルスコープを使いこなすコツです。


初心者として「レキシカルスコープ」で本当に押さえてほしいこと

レキシカルスコープは、
「スコープは“書かれた場所”で決まる」 というルール。

関数がどこから呼ばれるかではなく、
どこで定義されているかで、
見える変数が決まる。

内側の関数は、
自分が書かれている場所の外側スコープを“覚えている”。

このルールのおかげで、

コードの見た目の入れ子構造と、
スコープの入れ子構造が一致する
(=人間にとって理解しやすい)

クロージャのような「外側の変数を覚えた関数」が自然に作れる

という世界が成り立っています。

コードを読むとき・書くときに、
「この関数は“どこで書かれているか”?
そのとき“何が見えていたか”?」

を意識してみてください。

それができるようになると、
スコープの話は「難しい理論」ではなく、
「当たり前の空気感」として体に馴染んでいきます。

タイトルとURLをコピーしました