メインコンテンツまでスキップ

[Medium] 📄 var, let, const

概要

JavaScript には変数を宣言するための3つのキーワードがあります:varletconst。いずれも変数の宣言に使用されますが、スコープ、初期化、重複宣言、再代入、アクセスのタイミングなどの面で違いがあります。

主な違い

動作varletconst
スコープ関数スコープまたはグローバルブロックスコープブロックスコープ
初期化任意任意必須
重複宣言許可不許可不許可
再代入許可許可不許可
宣言前アクセスundefined を返すReferenceError 発生ReferenceError 発生

詳細説明

スコープ

var のスコープは関数スコープまたはグローバルスコープですが、letconst はブロックスコープ(関数、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

初期化

varlet は宣言時に初期化しなくても構いませんが、const は宣言時に必ず初期化する必要があります。

var varVariable;  // 有効
let letVariable; // 有効
const constVariable; // SyntaxError: Missing initializer in const declaration

重複宣言

同じスコープ内で、var は同じ変数の重複宣言を許可しますが、letconst は許可しません。

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

再代入

varlet で宣言された変数は再代入できますが、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 に初期化されます。一方、letconst で宣言された変数も 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 を出力

面接のポイント

この問題がテストするのは:

  1. var のスコープ:関数スコープ vs ブロックスコープ
  2. Event Loop:同期 vs 非同期実行
  3. Closure:関数が外部変数をどのようにキャプチャするか
  4. 解決策:複数の解法とその長所・短所の比較

回答する際は以下を推奨します:

  • まず正解を述べる(6 6 6 6 6)
  • 理由を説明する(var のスコープ + setTimeout の非同期)
  • 解決策を提供する(let を優先し、他の方法も説明する)
  • JavaScript の内部メカニズムへの理解を示す

ベストプラクティス

  1. 優先的に const を使用:再代入が不要な変数には const を使用することで、コードの可読性と保守性が向上します。
  2. 次に let を使用:再代入が必要な場合は let を使用します。
  3. var の使用を避ける:var のスコープと Hoisting の動作は予期しない問題を引き起こす可能性があるため、モダンな JavaScript 開発では使用を避けることを推奨します。
  4. ブラウザ互換性に注意:旧ブラウザのサポートが必要な場合は、Babel などのツールを使用して letconstvar にトランスパイルできます。

関連トピック