Back

JavaScript変数宣言:var、let、constの理解

JavaScript変数宣言:var、let、constの理解

モダンなJavaScript開発では、JavaScript変数宣言における明確性が求められています。しかし、多くの開発者は依然としてvarletconstの選択に苦労しています。ES6変数スコープを理解することは、単にルールを暗記することではありません。意図を伝え、バグを未然に防ぐコードを書くことが重要なのです。

重要なポイント

  • ブロックスコープ(let/const)は、関数スコープ(var)よりも優れた変数の封じ込めを提供します
  • Temporal Dead Zone(一時的デッドゾーン)は、初期化前の変数使用を防ぎます
  • constは再代入を防ぎますが、オブジェクトや配列の変更は防ぎません
  • モダンなJavaScriptでは、デフォルトでconstを使用し、必要に応じてletを使い、varは避けます

JavaScriptの3つの宣言キーワードの理解

JavaScriptは変数を宣言する3つの方法を提供しており、それぞれスコープ、巻き上げ(hoisting)、再代入に関して異なる動作をします。選択したキーワードは、JavaScriptエンジンとコードを読む他の開発者の両方に対して、異なる意図を伝えます。

ブロックスコープ vs 関数スコープ

モダンなES6変数スコープとレガシーパターンの根本的な違いは、変数がどのように封じ込められるかにあります:

function processData() {
    if (true) {
        var x = 1;  // 関数スコープ
        let y = 2;  // ブロックスコープ
        const z = 3; // ブロックスコープ
    }
    console.log(x); // 1 (アクセス可能)
    console.log(y); // ReferenceError
    console.log(z); // ReferenceError
}

varを使用すると、変数はブロックから漏れ出しますが、関数内には留まります。letconstの両方はブロックの境界を尊重します。ループ、条件文、プレーンブロックを含む、あらゆる中括弧が新しいスコープを作成します。

Temporal Dead Zone(一時的デッドゾーン)の説明

letconstによるJavaScript変数宣言は巻き上げられますが、宣言に到達するまで初期化されません。これがTemporal Dead Zone(TDZ)を作り出します:

console.log(myVar);   // undefined (巻き上げられ、初期化済み)
console.log(myLet);   // ReferenceError (TDZ)
console.log(myConst); // ReferenceError (TDZ)

var myVar = 1;
let myLet = 2;
const myConst = 3;

TDZは、varの動作がしばしば引き起こしていた微妙なバグを防ぎます。適切に初期化される前に変数を誤って使用することはできません。エンジンは即座にエラーをスローします。

再代入 vs 変更(Mutation)

多くの開発者が見逃す重要な違い:constは再代入を防ぎますが、変更は防ぎません:

const user = { name: 'Alice' };
user.name = 'Bob';          // 許可される(変更)
user = { name: 'Charlie' }; // TypeError (再代入)

const scores = [95, 87];
scores.push(91);            // 許可される
scores = [100, 100];        // TypeError

真のイミュータビリティ(不変性)を実現するには、constObject.freeze()やイミュータブルデータライブラリと組み合わせます。宣言キーワード単体ではデータをイミュータブルにしません。バインディングをイミュータブルにするのです。

変数宣言のモダンなベストプラクティス

今日のJavaScriptコードベースは、明確な優先順位に従っています:

  1. デフォルトでconstを使用: 再代入されない値に使用します。これには、変更する予定のオブジェクトや配列を含むほとんどの変数が含まれます。

  2. 再代入にはletを使用: ループカウンター、アキュムレーター変数、本当に再バインディングが必要な値に使用します。

  3. 新しいコードではvarを避ける: varletよりも利点を提供するモダンなユースケースはありません。

モダンなツールはこれらのパターンを強制します。ESLintのno-varルールとprefer-constルールは、違反を自動的に検出します。TypeScriptはES6変数スコープルールを基本として扱い、ブロックスコープをデフォルトのメンタルモデルにしています。

フレームワークの考慮事項

Reactフックは、JavaScript変数宣言が予測可能に動作することに依存しています:

function Counter() {
    const [count, setCount] = useState(0); // バインディングにはconst
    
    // 誤り:再代入できない
    // count = count + 1;
    
    // 正しい:セッターを使用
    setCount(count + 1);
}

VueのComposition APIや他のモダンなフレームワークも同様のパターンに従っており、リアクティビティシステムが再代入ではなくメソッドを通じて更新を処理するため、constが主流となっています。

レガシーコードが依然としてvarを使用する理由

古いコードベースでは、歴史的な理由からvarに遭遇するでしょう。ES6(2015年)以前は、それが唯一の選択肢でした。移行には慎重なテストが必要です。なぜなら:

  • ループ内のvarletに変更すると、クロージャの動作によってバグが修正されたり、逆に発生したりする可能性があります
  • 一部のレガシーコードは意図的にvarの関数スコープ巻き上げを利用しています
  • 古いビルドツールはES6変数スコープを正しくトランスパイルできない可能性があります

リファクタリングする際は、徹底的なテストカバレッジとともにjscodeshiftのような自動化ツールを使用してください。何千もの宣言を手動で変換しないでください。機械的な変更はツールに任せ、動作を検証することに集中しましょう。

まとめ

モダンなコードにおけるJavaScript変数宣言は、シンプルなルールに従います:デフォルトでconstを使用し、再代入が必要な場合はletを使用し、varは決して使用しません。これはトレンドに従うことではありません。意図を明確に表現し、ES6変数スコープを活用してバグのカテゴリ全体を防ぐコードを書くことです。Temporal Dead Zone、ブロックスコープ、再代入ルールは障害ではありません。より保守性の高いJavaScriptへと導くガードレールなのです。

よくある質問

はい、constは変数自体の再代入のみを防ぎ、内容の変更は防ぎません。constで宣言されたオブジェクトのプロパティや配列の要素を追加、削除、変更することができます。変数は常にメモリ内の同じオブジェクトまたは配列を参照します。

Temporal Dead Zoneにより、ReferenceErrorが発生します。宣言前にアクセスするとundefinedを返すvarとは異なり、letとconst変数はコードが宣言行に到達するまで未初期化状態で存在します。

慎重なテストを行った上でのみ実施してください。一般的には有益ですが、ループ内のvarをletに変更すると、クロージャの動作が変わる可能性があります。バグの発生を避けるため、手動変更ではなく包括的なテストカバレッジを備えた自動リファクタリングツールを使用してください。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay