[Medium] 📄 var, let, const
概要
JavaScript には変数を宣言するための3つのキーワードがあります:var、let、const。いずれも変数の宣言に使用されますが、スコープ、初期化、重複宣言、再代入、アクセスのタイミングなどの面で違いがあります。
主な違い
| 動作 | var | let | const |
|---|---|---|---|
| スコープ | 関数スコープまたはグローバル | ブロックスコープ | ブロックスコープ |
| 初期化 | 任意 | 任意 | 必須 |
| 重複宣言 | 許可 | 不許可 | 不許可 |
| 再代入 | 許可 | 許可 | 不許可 |
| 宣言前アクセス | undefined を返す | ReferenceError 発生 | ReferenceError 発生 |
詳細説明
スコープ
var のスコープは関数スコープまたはグローバルスコープですが、let と const はブロックスコープ(関数、if-else ブロック、for ループを含む)です。
function scopeExample() {
var varVariable = 'var';
let letVariable = 'let';
const constVariable = 'const';
console.log(varVariable); // 'var'
console.log(letVariable); // 'let'
console.log(constVariable); // 'const'
}
scopeExample();
console.log(varVariable); // ReferenceError: varVariable is not defined
console.log(letVariable); // ReferenceError: letVariable is not defined
console.log(constVariable); // ReferenceError: constVariable is not defined
if (true) {
var varInBlock = 'var in block';
let letInBlock = 'let in block';
const constInBlock = 'const in block';
}
console.log(varInBlock); // 'var in block'
console.log(letInBlock); // ReferenceError: letInBlock is not defined
console.log(constInBlock); // ReferenceError: constInBlock is not defined
初期化
var と let は宣言時に初期化しなくても構いませんが、const は宣言時に必ず初期化する必要があります。
var varVariable; // 有効
let letVariable; // 有効
const constVariable; // SyntaxError: Missing initializer in const declaration
重複宣言
同じスコープ内で、var は同じ変数の重複宣言を許可しますが、let と const は許可しません。
var x = 1;
var x = 2; // 有効、x は 2 になる
let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared
const z = 1;
const z = 2; // SyntaxError: Identifier 'z' has already been declared
再代入
var と let で宣言された変数は再代入できますが、const で宣言された変数は再代入できません。
var x = 1;
x = 2; // 有効
let y = 1;
y = 2; // 有効
const z = 1;
z = 2; // TypeError: Assignment to a constant variable
注意:const で宣言された変数は再代入できませんが、オブジェクトや配列の場合、その内容は変更可能です。
const obj = { key: 'value' };
obj.key = 'new value'; // 有効
console.log(obj); // { key: 'new value' }
const arr = [1, 2, 3];
arr.push(4); // 有効
console.log(arr); // [1, 2, 3, 4]
宣言前アクセス(Temporal Dead Zone)
var で宣言された変数は Hoisting され自動的に undefined に初期化されます。一方、let と const で宣言された変数も Hoisting されますが、初期化はされないため、宣言前にアクセスすると ReferenceError がスローされます。
console.log(x); // undefined
var x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
console.log(z); // ReferenceError: Cannot access 'z' before initialization
const z = 5;
面接問題
問題:setTimeout + var の典型的な罠
以下のコードの出力結果を判定してください:
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
誤答(よくある誤解)
多くの人が出力は 1 2 3 4 5 だと思います。
実際の出力
6
6
6
6
6
なぜ?
この問題は3つのコアコンセプトに関わります:
1. var の関数スコープ
// var はループ内でブロックスコープを作成しない
for (var i = 1; i <= 5; i++) {
// i は外側のスコープにあり、全てのイテレーションが同じ i を共有する
}
console.log(i); // 6(ループ終了後の i の値)
// var の場合
{
var i;
i = 1;
i = 2;
i = 3;
i = 4; // ループ終了
}
2. setTimeout の非同期実行
// setTimeout は非同期で、現在の同期コードの実行完了後に実行される
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
// ここのコードは Event Loop のタスクキューに入れられる
console.log(i);
}, 0);
}
// ループが先に完了し(i が 6 になる)、その後 setTimeout のコールバックが実行される
3. Closure の参照
// 全ての setTimeout コールバック関数が同じ i を参照している
// コールバックが実行される時、i は既に 6 になっている
解決策
方法 1:let を使用(推奨)★
for (let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
// 出力:1 2 3 4 5
// let の場合
{
let i = 1; // 第1イテレーションの i
}
{
let i = 2; // 第2イテレーションの i
}
{
let i = 3; // 第3イテレーションの i
}
原理:let は各イテレーションで新しいブロックスコープを作成し、各 setTimeout コールバックが現在のイテレーションの i の値をキャプチャします。
// 等価
{
let i = 1;
setTimeout(function () {
console.log(i);
}, 0);
}
{
let i = 2;
setTimeout(function () {
console.log(i);
}, 0);
}
// ... 以下同様
方法 2:IIFE(即時実行関数)を使用
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 0);
})(i);
}
// 出力:1 2 3 4 5
原理:IIFE が新しい関数スコープを作成し、各イテレーションで現在の i の値がパラメータ j として渡され、Closure を形成します。
方法 3:setTimeout の第3引数を使用
for (var i = 1; i <= 5; i++) {
setTimeout(
function (j) {
console.log(j);
},
0,
i
); // 第3引数がコールバック関数に渡される
}
// 出力:1 2 3 4 5
原理:setTimeout の第3引数以降はコールバック関数の引数として渡されます。
方法 4:bind を使用
for (var i = 1; i <= 5; i++) {
setTimeout(
function (j) {
console.log(j);
}.bind(null, i),
0
);
}
// 出力:1 2 3 4 5
原理:bind は新しい関数を作成し、現在の i の値を引数としてバインドします。
方法の比較
| 方法 | メリット | デメリット | 推奨度 |
|---|---|---|---|
let | 簡潔、モダン、分かりやすい | ES6+ | 5/5 強く推奨 |
| IIFE | 互換性が良い | 構文が複雑 | 3/5 検討可 |
| setTimeout 引数 | シンプルで直接的 | あまり知られていない | 4/5 推奨 |
bind | 関数型スタイル | 可読性がやや劣る | 3/5 検討可 |
発展問題
Q1: 次のように変更したら?
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
答え:毎秒 6 が出力され、合計5回出力されます(それぞれ1秒、2秒、3秒、4秒、5秒時に出力)。
Q2: 毎秒順番に 1、2、3、4、5 を出力したい場合は?
for (let i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
// 1秒後に 1 を出力
// 2秒後に 2 を出力
// 3秒後に 3 を出力
// 4秒後に 4 を出力
// 5秒後に 5 を出力
面接のポイント
この問題がテストするのは:
- var のスコープ:関数スコープ vs ブロックスコープ
- Event Loop:同期 vs 非同期実行
- Closure:関数が外部変数をどのようにキャプチャするか
- 解決策:複数の解法とその長所・短所の比較
回答する際は以下を推奨します:
- まず正解を述べる(6 6 6 6 6)
- 理由を説明する(var のスコープ + setTimeout の非同期)
- 解決策を提供する(let を優先し、他の方法も説明する)
- JavaScript の内部メカニズムへの理解を示す
ベストプラクティス
- 優先的に
constを使用:再代入が不要な変数にはconstを使用することで、コードの可読性と保守性が向上します。 - 次に
letを使用:再代入が必要な場合はletを使用します。 varの使用を避ける:varのスコープと Hoisting の動作は予期しない問題を引き起こす可能性があるため、モダンな JavaScript 開発では使用を避けることを推奨します。- ブラウザ互換性に注意:旧ブラウザのサポートが必要な場合は、Babel などのツールを使用して
letとconstをvarにトランスパイルできます。