let、var、const ― 変数宣言の違い
このトピックを終えると
var、let、const の違いをスコープと再代入の観点から説明でき、どのような状況で何を使うべきかを判断できるようになります。
3つの宣言キーワード
JavaScript で変数を宣言する方法は3つあります。
var name = "철수"; // ES5 (古い方法)
let age = 25; // ES6 (2015~)
const PI = 3.14159; // ES6 (2015~)これら3つは、「値を格納する名前を作成する」という点では同じです。違いは、スコープ(どこまで見えるか)と、再代入(値を変更できるか)にあります。
再代入の可否
let count = 0;
count = 1; // ✅ let は再代入可能
const MAX = 100;
MAX = 200; // ❌ TypeError: Assignment to constant variableconst は宣言後に再代入できません。一度代入した値を変更することはできません。let と var は再代入可能です。
注意:const は「値が不変」ではなく、「変数バインディングが不変」です。オブジェクトや配列の内部は変更可能です。
const user = { name: "철수" };
user.name = "영희"; // ✅ オブジェクト内部の変更は可能
user = {}; // ❌ 変数自体を別の値に置き換えることは不可スコープ — var vs let/const
var は 関数スコープです。if文やfor文のブロック({})を無視します。
function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 — ifブロックの外でもアクセス可能!
}let/const は ブロックスコープです。宣言されたブロック({})内でのみ存在します。
function example() {
if (true) {
let y = 10;
}
console.log(y); // ReferenceError: y is not defined
}ブロックスコープの方が直感的です。宣言した中括弧内でのみ有効で、外では消えます。
for文での違い
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 出力: 3, 3, 3 — すべて同じ i を参照var で宣言した i は for文ブロックを無視するため、setTimeout が実行されるときにはすでに i は 3 になっています。
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 出力: 0, 1, 2 — 各反復ごとに新しい ilet は各反復ごとに新しいブロックスコープを作成するため、それぞれの i が独立して存在します。これが let と var の実質的に最も重要な違いです。
ホイスティング
ホイスティングとは、宣言がコードの最上部に引き上げられる現象です。
console.log(a); // undefined (エラーではない!)
var a = 5;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;var は宣言がホイスティングされ、undefined で初期化されます。そのため、宣言前にアクセスしてもエラーになりません。これはバグを見つけるのが難しくなります。
let と const もホイスティングされますが、初期化前にアクセスするとエラーを発生させます。この期間を TDZ (Temporal Dead Zone) と呼びます。エラーが発生する方がデバッグに有利です。
実務ガイドライン
// 1. 基本は const
const API_URL = "https://api.example.com";
const users = [];
// 2. 値が変わる場合にのみ let
let count = 0;
count++;
// 3. var は使わない
// (レガシーコードでのみ見られる)| キーワード | スコープ | 再代入 | ホイスティング | 使用するタイミング |
|---|---|---|---|---|
| var | 関数 | ✅ | undefined で初期化 | 使わない |
| let | ブロック | ✅ | TDZ (エラー) | 値が変わるとき |
| const | ブロック | ❌ | TDZ (エラー) | デフォルト |
モダン JavaScript では、const をデフォルトとして使用し、再代入が必要な場合にのみ let に変更します。var は歴史的な理由で残っているだけで、新しいコードで使う理由はありません。
クロージャーと var の有名なバグ
var の関数スコープが引き起こす最も有名なバグ:
// 5つのボタンにそれぞれ異なる数字を表示したい
for (var i = 0; i < 5; i++) {
document.getElementById("btn" + i).onclick = function() {
alert(i); // すべて 5 を表示
};
}すべてのボタンが 5 を表示します。クリックする時点では i はすでに 5 になっており、すべてのコールバックが同じ i を参照しているためです。
// let に変更すると解決
for (let i = 0; i < 5; i++) {
document.getElementById("btn" + i).onclick = function() {
alert(i); // 0, 1, 2, 3, 4
};
}let は各反復ごとに新しいスコープを作成するため、各コールバックがそれぞれ独自の i を持ちます。ES6 以前は IIFE (即時実行関数) で回避していましたが、let の登場によりこのパターンは不要になりました。
関数宣言における var/let/const
関数を作成するときにも、宣言方法によって違いがあります。
// 関数宣言 — ホイスティングされる
greet(); // ✅ 動作する
function greet() {
console.log("こんにちは");
}
// const 関数式 — ホイスティングされない
hello(); // ❌ ReferenceError
const hello = () => {
console.log("こんにちは");
};関数宣言はコードのどこからでも呼び出すことができますが、const で宣言した関数は宣言以降でのみ呼び出すことができます。プロジェクトでは通常、一貫した方法を使用します。
グローバルスコープでの違い
// ブラウザ環境で
var globalVar = "私は var";
let globalLet = "私は let";
console.log(window.globalVar); // "私は var" — window オブジェクトに追加される
console.log(window.globalLet); // undefined — window に追加されないvar で宣言したグローバル変数は、window オブジェクトのプロパティになります。これはライブラリ間の名前の衝突の原因となります。let と const はグローバルスコープで宣言しても、window に追加されません。
よくある間違いと解決策
// 間違い 1: const オブジェクトの内部を変更することは可能
const config = { debug: true };
config.debug = false; // ✅ 可能 — オブジェクト内部の変更
config = {}; // ❌ 不可能 — 変数の再代入
// 間違い 2: for文に const を使う
for (const i = 0; i < 3; i++) { // ❌ i++ で再代入エラー
console.log(i);
}
// for...of では const を使用可能 (各反復で新しいバインディング)
for (const item of [1, 2, 3]) { // ✅
console.log(item);
}Object.freeze — 真に不変にする
const オブジェクトの内部を本当に変更できなくするには:
const config = Object.freeze({
apiUrl: "https://api.example.com",
maxRetries: 3
});
config.apiUrl = "changed"; // 静かに無視される (strict mode ではエラー)
console.log(config.apiUrl); // "https://api.example.com"Object.freeze() は、オブジェクトのプロパティの追加、変更、削除を防止します。ただし、浅いフリーズであるため、ネストされたオブジェクトの内部は引き続き変更可能です。
コードを読むときに const が見えれば、「この値は変わらないんだ」とすぐにわかります。この予測可能性は、プロジェクトが大きくなるほど重要になります。