JavaScript 入門

JavaScript スコープ

スコープは、コードの異なる部分における変数の可視性(アクセスできる範囲)を決定します。

スコープをマスターすることは、名前の衝突を防ぎ、データを効率的に管理し、モジュール化されたアプリケーションを構築するための基礎となります。

本章では、グローバルスコープとローカルスコープの概念を深く掘り下げ、JavaScriptのファンクション内における変数の振る舞いを理解するための強固な土台を築きます。

1. スコープ(Scope)を理解する

JavaScriptのコンテキストにおいて、スコープは変数のアクセシビリティ、あるいは可視性を定義します。本質的には、コードのどこで特定の変数を参照・修正できるかを決定するものです。

スコープを理解することは非常に重要です。なぜなら、意図しないサイドエフェクト(副作用)を防ぎ、コードの動作を予測可能なものにできるからです。JavaScriptには主に「グローバルスコープ」と「ローカルスコープ」の2つのタイプがあります。

2. グローバルスコープ

あらゆるファンクションやコードブロックの外で宣言された変数は、グローバルスコープを持ちます。これは、ファンクションの内部を含め、JavaScriptコードのどこからでもアクセス・修正が可能であることを意味します。

例:

// グローバル変数を宣言
var globalVariable = "私はグローバル変数です";

function myFunction() {
  // ファンクションの内部からグローバル変数にアクセス
  console.log(globalVariable); // 出力: 私はグローバル変数です
}

myFunction();
console.log(globalVariable); // 出力: 私はグローバル変数です

この例では、globalVariablemyFunction の内部からも外部からもアクセス可能です。

重要な考慮事項:

  • グローバル変数は一見便利ですが、多用すると名前の衝突を引き起こし、メンテナンスが困難になります。コードの異なる部分で同じグローバル変数名を使用すると、知らぬ間に値を上書きしてしまい、予期せぬ動作を招く恐れがあります。
  • Webブラウザにおいて、グローバルスコープは window オブジェクトそのものです。var を使ってグローバル変数を宣言すると、それは window オブジェクトのプロパティになります(例:window.globalVariable)。
  • ファンクションやブロックの外で letconst を使った場合もグローバル変数が作成されますが、これらは window オブジェクトのプロパティにはなりません。これは微妙ですが重要な違いです。

グローバル変数の潜在的な問題を示す例:

var counter = 0; // グローバルカウンター

function incrementCounter() {
  counter++;
  console.log("incrementCounter 内部のカウンター:", counter);
}

function resetCounter() {
  counter = 0;
  console.log("resetCounter 内部のカウンター:", counter);
}

incrementCounter(); // 出力: 1
incrementCounter(); // 出力: 2
resetCounter();     // 出力: 0
incrementCounter(); // 出力: 1

この例では、incrementCounterresetCounter は同じグローバル変数 counter を修正しています。大規模なアプリケーションの異なる部分でこれらのファンクションが使用されると、一方の変化が他方に影響を与え、バグの原因となります。

3. ローカルスコープ

ファンクションの内部で宣言された変数は、ローカルスコープ(関数スコープ)を持ちます。これは、その変数がそのファンクションの内部でしかアクセスできないことを意味し、外部からは不可視となります。

例:

function myFunction() {
  // ローカル変数を宣言
  var localVariable = "私はローカル変数です";
  console.log(localVariable); // 出力: 私はローカル変数です
}

myFunction();
// ファンクションの外部からアクセスしようとするとエラーになります
// console.log(localVariable); // ReferenceError: localVariable is not defined

この例では、localVariablemyFunction 内部でのみ有効です。

スコープと var, let, const:

  • var: ファンクション内部で宣言された場合、関数スコープの変数を作成します。これは、宣言がどこであっても、そのファンクション内であればどこからでもアクセス可能です。
  • letconst: これらもファンクション内部で宣言されれば関数スコープとなりますが、同時にブロックスコープ(Block-Scoped)でもあります。つまり、定義されたコードブロック(if 文や for ループの中括弧内など)に限定されます。

3.1 var, let, const の違いをデモンストレーションする例

function testScope() {
  if (true) {
    var varVariable = "var 変数";
    let letVariable = "let 変数";
    const constVariable = "const 変数";
  }

  console.log(varVariable);   // 出力: var 変数 (関数スコープのため、ifブロック外に漏れ出す)
  // console.log(letVariable);   // エラー: letVariable は未定義 (ブロックスコープにより制限)
  // console.log(constVariable); // エラー: constVariable は未定義 (ブロックスコープにより制限)
}

testScope();

varVariableif ブロックの外でもアクセス可能ですが、letconst で宣言された変数はブロックの外ではアクセスできません。

4. ブロックスコープ

前述の通り、letconst はブロックスコープの概念を導入しました。ブロック(Block)とは、花括弧 {} で囲まれたコードの断片を指します(if 文、for ループ、while ループ、あるいは独立した {} など)。

{
  let blockVariable = "私はブロックレベルの変数です";
  console.log(blockVariable); // 出力: 私はブロックレベルの変数です
}
// console.log(blockVariable); // ReferenceError: blockVariable is not defined

なぜブロックスコープが重要なのか?
名前の衝突を避け、コードの動作を予測しやすくするためです。異なるブロックであれば、互いに干渉することなく同じ名前の変数を使用できるため、複雑なファンクションやループ内での一時的な変数管理に非常に役立ちます。

5. スコープチェーンとレキシカルスコープ

JavaScriptで変数にアクセスしようとすると、エンジンはまず現在のスコープからその変数を探します。そこに見つからない場合、一つ外側のスコープへと探しに行き、これをグローバルスコープに到達するまで繰り返します。

このスコープの連なりをスコープチェーン(Scope Chain)と呼びます。

スコープチェーンはコードのレキシカル構造(Lexical Structure)、つまりファンクションやブロックがコード内のどの物理的な位置に記述されているかによって決定されます。これをレキシカルスコープ(Lexical Scoping)と呼びます。

var globalVariable = "グローバル";

function outerFunction() {
  var outerVariable = "外層";

  function innerFunction() {
    var innerVariable = "内層";
    console.log(globalVariable);  // グローバルスコープからアクセス
    console.log(outerVariable);   // outerFunction のスコープからアクセス
    console.log(innerVariable);   // innerFunction のスコープからアクセス
  }

  innerFunction();
}

outerFunction();

この階層構造により、内部のファンクションは外部の変数にアクセスできますが、その逆はできません。

6. シャドウイング

内部スコープと外部スコープで同じ名前の変数が宣言されている場合、内部スコープの変数が外部スコープの変数を遮蔽(Shadowing)します。内部スコープでは内部の変数が優先され、同名の外部変数には直接アクセスできなくなります。

var myVariable = "グローバル変数";

function myFunction() {
  var myVariable = "ローカル変数"; // グローバルの myVariable をシャドウイング
  console.log(myVariable); // 出力: ローカル変数
}

myFunction();
console.log(myVariable); // 出力: グローバル変数

7. 実践ケースとデモンストレーション

7.1 ケース 1:ループ内での let と const

for (let i = 0; i < 5; i++) {
  console.log("ループ内部:", i);
}
// console.log("ループ外部:", i); // エラー: i は未定義

let を使うことで、ループカウンタ i の影響範囲をループ内だけに限定できます。

7.2 ケース 2:ブロックスコープを利用した名前衝突の回避

function processData(data) {
  if (data.length > 0) {
    let result = "データを処理中...";
    console.log(result);
  } else {
    let result = "処理するデータがありません。";
    console.log(result);
  }
  // console.log(result); // エラー: result は未定義
}

同じ result という名前を別々のブロックで独立して使用しても、互いに干渉しません。

7.3 ケース 3:クロージャ(Closures)とスコープ

クロージャは、ファンクションがその周囲のスコープの変数を、外側のファンクションの実行が終わった後でも保持し続ける仕組みです。

function outerFunction(outerVariable) {
  function innerFunction() {
    console.log(outerVariable); // 外側のスコープの変数にアクセス
  }
  return innerFunction;
}

var myInnerFunction = outerFunction("外側からの挨拶!");
myInnerFunction(); // 出力: 外側からの挨拶!

innerFunction はクロージャとなり、outerFunction が終了した後も outerVariable を「記憶」しています。これはスコープチェーンが保持されているためです。