概要
JavaScriptにおける変数宣言には、var、let、const の3つのキーワードが存在する。
var はJavaScript誕生当初から存在するキーワードであり、関数スコープまたはグローバルスコープで変数を宣言する。
一方、let と const は2015年に策定されたES2015 (ES6) で新たに導入されたキーワードであり、ブロックスコープを持つ。
これら3つのキーワードは、スコープ、ホイスティング (変数の巻き上げ)、再宣言・再代入の可否という3つの観点で大きく異なる。
var はブロックスコープを持たないため、ループや条件分岐の内部で宣言した変数が外部に漏れ出し、予期しない動作を引き起こす問題がある。
また、宣言前のアクセスが undefined を返すという挙動も、バグの温床となりやすい。
let と const はこれらの問題を解消するために設計されており、現代的なJavaScript開発ではこの2つを用いることが標準となっている。
原則として const をデフォルトの選択肢として使用し、再代入が必要な場面でのみ let を使用することが広く推奨されている。
var は新規開発では使用せず、既存のレガシーコードを読み解く時の知識として理解しておくことが求められる。
let
基本的な使用方法
let はブロックスコープを持ち、再代入可能な変数を宣言するキーワードである。
初期値なしで宣言することも、宣言と同時に初期値を設定することも可能である。
let name;
let name = value;
let name1 = value1, name2 = value2;
使用例を以下に示す。
let count = 0;
count = 1; // 再代入可能
console.log(count); // 1
let message;
message = "Hello, World!";
console.log(message); // "Hello, World!"
再代入と再宣言
let は再代入が可能であるが、同一スコープ内での再宣言は SyntaxError となる。
let x = 1;
x = 2; // OK : 再代入は可能
console.log(x); // 2
let x = 3; // SyntaxError: Identifier 'x' has already been declared
ただし、異なるブロックスコープでは同じ名前の変数を宣言できる。
const x = "outer";
{
const x = "inner"; // OK : 別のブロックスコープ
console.log(x); // "inner"
}
console.log(x); // "outer"
ブロックスコープ
let はブロックスコープを持つ。
if、for、while、{} 等のブロック内で宣言した変数は、そのブロック外からアクセスできない。
function letTest() {
let x = 1;
{
let x = 2; // 別のブロックスコープの変数
console.log(x); // 2
}
console.log(x); // 1
}
for (let i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
console.log(i); // ReferenceError: i is not defined
また、let はトップレベルで宣言してもグローバルオブジェクト (globalThis) のプロパティにはならない。
var globalVar = "var";
let globalLet = "let";
console.log(globalThis.globalVar); // "var"
console.log(globalThis.globalLet); // undefined
const
基本的な使用方法
const はブロックスコープを持ち、再代入不可の定数を宣言するキーワードである。
const は宣言と同時に必ず初期値を指定しなければならない。
初期化を省略すると SyntaxError となる。
const name = value;
const PI = 3.14159;
const MAX_SIZE = 100;
const APP_NAME = "MyApp";
const FOO; // SyntaxError: Missing initializer in const declaration
再代入の禁止
const で宣言した変数への再代入は TypeError となる。
const PI = 3.14159;
PI = 3.14; // TypeError: Assignment to constant variable.
同一スコープ内での再宣言も SyntaxError となる。
const y = 1;
const y = 2; // SyntaxError: Identifier 'y' has already been declared
constとオブジェクト (参照の不変性)
const が保証するのは変数への参照の不変性のみである。
参照先のオブジェクトや配列の中身 (プロパティや要素) は変更できる。
const obj = {
name: "Alice",
age: 30
};
obj.name = "Bob"; // OK : プロパティの変更は可能
obj.age = 31; // OK
obj = { name: "Charlie" }; // TypeError - 再代入は不可
const arr = [1, 2, 3];
arr[0] = 99; // OK : 要素の変更は可能
arr.push(4); // OK : メソッドの呼び出しは可能
arr = [4, 5, 6]; // TypeError : 再代入は不可
Object.freeze
オブジェクトの中身を変更させたくない場合は、Object.freeze() を使用する。
Object.freeze() を適用すると、プロパティの追加・削除・変更がすべて禁止される。
ただし、Object.freeze() は浅いフリーズ (Shallow Freeze) であるため、ネストされたオブジェクトや配列は保護されない。
const frozenObj = Object.freeze({
name: "Alice",
age: 30
});
frozenObj.name = "Bob"; // 失敗 (strict mode では TypeError)
frozenObj.newProp = "value"; // 失敗 : プロパティの追加も不可
console.log(frozenObj.name); // "Alice"
// 浅いフリーズの制限: ネストされたオブジェクトは変更可能
const obj = Object.freeze({
level1: {
level2: "value"
}
});
obj.level1.level2 = "newValue"; // OK : ネストされたオブジェクトは保護されない
console.log(obj.level1.level2); // "newValue"
ネストされたオブジェクトも含めて完全に不変にするには、再帰的にフリーズする深いフリーズ (Deep Freeze) を実装する。
function deepFreeze(object) {
const propNames = Reflect.ownKeys(object);
for (const name of propNames) {
const value = object[name];
if ((value && typeof value === "object") || typeof value === "function") {
deepFreeze(value);
}
}
return Object.freeze(object);
}
const deepFrozenObj = deepFreeze({
level1: {
level2: "value"
}
});
deepFrozenObj.level1.level2 = "newValue"; // 失敗
console.log(deepFrozenObj.level1.level2); // "value"
ブロックスコープ
const のスコープは、let と同様にブロックスコープである。
const MY_FAV = 7;
if (MY_FAV === 7) {
const MY_FAV = 20; // 新しいブロックスコープ内の別の変数
console.log(MY_FAV); // 20
}
console.log(MY_FAV); // 7
var
基本的な使用方法
var は関数スコープまたはグローバルスコープで変数を宣言するキーワードである。
ES2015以前から存在する古いキーワードであり、全てのWebブラウザで広くサポートされている。
var name;
var name = value;
var name1 = value1, name2 = value2;
var は同一スコープ内での再宣言が可能であり、エラーにはならない。
var a = 1;
var a = 2; // エラーにならない
console.log(a); // 2
関数スコープ
var は関数スコープを持つ。
ブロック (if、for、while 等) は、var のスコープを形成しない。
ブロック内で宣言した var 変数は、そのブロックの外からもアクセスできる。
function functionScope() {
if (true) {
var x = "inside";
}
console.log(x); // "inside" - if ブロック外でもアクセス可能
}
functionScope();
for (var i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
console.log(i); // 3 - ループ外でもアクセス可能
また、トップレベルでの var 宣言はグローバルオブジェクト (globalThis) のプロパティとして追加される。
var x = 1;
console.log(globalThis.x); // 1
delete x; // 削除できない (strict modeでは、TypeError)
console.log(globalThis.x); // 1
varの問題点
var の使用はいくつかの問題を引き起こすため、現代的なJavaScript開発では使用が推奨されない。
ループでのクロージャ問題を以下に示す。
var はブロックスコープを持たないため、ループ内でクロージャを使用すると、全ての関数が同じ変数を参照してしまう。
// 問題のあるコード (varを使用)
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs[0](); // 3 (期待値: 0)
funcs[1](); // 3 (期待値: 1)
funcs[2](); // 3 (期待値: 2)
// 解決策: letを使用
let funcs2 = [];
for (let i = 0; i < 3; i++) {
funcs2.push(function() {
console.log(i);
});
}
funcs2[0](); // 0
funcs2[1](); // 1
funcs2[2](); // 2
変数の巻き上げによる予期しない動作を以下に示す。
var はホイスティングにより undefined で初期化されるため、宣言前にアクセスしてもエラーにならず、デバッグが困難になる。
function unexpected() {
console.log(x); // undefined (エラーにならない)
if (false) {
var x = 5; // このブロックは実行されないが、宣言は巻き上げられる
}
console.log(x); // undefined
}
グローバルオブジェクトへのプロパティ追加を以下に示す。
トップレベルの var 宣言はグローバルオブジェクトにプロパティとして追加されて、グローバル汚染を引き起こす可能性がある。
var message = "global";
console.log(window.message); // "global" : ブラウザ環境
let safe = "no pollution";
console.log(window.safe); // undefined
スコープの比較
下表に、let、const、var のスコープの違いを示す。
| キーワード | スコープの種類 | ブロック内での宣言 | グローバルオブジェクトへの追加 |
|---|---|---|---|
var |
関数スコープ / グローバルスコープ | ブロック外からアクセス可能 | あり |
let |
ブロックスコープ | ブロック外からアクセス不可 | なし |
const |
ブロックスコープ | ブロック外からアクセス不可 | なし |
スコープの挙動を示すコード例を以下に示す。
// var: 関数スコープ
function testVar() {
if (true) {
var x = "var";
}
console.log(x); // "var": アクセス可能
}
// let: ブロックスコープ
function testLet() {
if (true) {
let y = "let";
}
console.log(y); // ReferenceError: y is not defined
}
// const: ブロックスコープ
function testConst() {
if (true) {
const z = "const";
}
console.log(z); // ReferenceError: z is not defined
}
ホイスティング
ホイスティング (変数の巻き上げ) とは、JavaScriptエンジンがプログラムの実行前に変数・関数・クラスの宣言を処理する仕組みのことである。
変数の使用箇所よりも前に宣言が処理されるが、var と let / const では挙動が大きく異なる。
varのホイスティング
var の宣言は、関数またはグローバルスコープの先頭に巻き上げられ、自動的に undefined で初期化される。
そのため、宣言前に変数にアクセスしてもエラーにはならず、undefined が返される。
console.log(x); // undefined (エラーではない)
var x = 5;
console.log(x); // 5
上記のコードは、JavaScriptエンジンによって内部的に以下のように処理される。
var x; // ホイスティング: undefinedで初期化
console.log(x); // undefined
x = 5; // 代入
console.log(x); // 5
let / constのホイスティングとTDZ
let と const も技術的にはホイストされるが、undefined では初期化されない。
代わりに、ブロックの開始から宣言文に到達するまでの間、Temporal Dead Zone (TDZ : 一時的デッドゾーン) と呼ばれる状態に置かれる。
TDZの期間中に変数にアクセスすると ReferenceError が発生する。
// letのTDZ
{
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;
}
// constのTDZ
{
console.log(y); // ReferenceError: Cannot access 'y' before initialization
const y = 10;
}
// varとの比較
{
console.log(z); // undefined (エラーではない)
var z = 15;
}
TDZは、外側のスコープに同名の変数が存在する場合にも注意が必要である。
内側のブロックで let / const を宣言すると、そのブロック全体がTDZの影響を受ける。
function test() {
var foo = 33;
if (foo) {
let foo = foo + 55; // ReferenceError
// 右辺の foo は内側のブロックの TDZ 内にあるためアクセスできない
}
}
下表に、ホイスティングの挙動比較を示す。
| キーワード | ホイスティング | 初期値 | 宣言前のアクセス |
|---|---|---|---|
var |
あり | undefined | undefined を返す |
let |
あり (TDZ) | 初期化されない | ReferenceError |
const |
あり (TDZ) | 初期化されない | ReferenceError |
let / const / var の比較
let、const、var の特性を総合的に比較した表を以下に示す。
| 特性 | var |
let |
const
|
|---|---|---|---|
| スコープ | 関数 / グローバル | ブロック | ブロック |
| ホイスティング | あり (undefined で初期化) | あり (TDZ) | あり (TDZ) |
| 宣言前のアクセス | undefined を返す | ReferenceError | ReferenceError |
| 再代入 | 可能 | 可能 | 不可 |
| 再宣言 | 可能 | 不可 | 不可 |
| 初期化の省略 | 可能 | 可能 | 不可 (SyntaxError) |
| グローバルオブジェクトへの追加 | あり | なし | なし |
| 導入バージョン | ES1 (初期) | ES2015 (ES6) | ES2015 (ES6) |
推奨される使用方法
現代的なJavaScript開発では、以下の原則に従って変数宣言のキーワードを選択することが推奨される。
constをデフォルトとして使用する。- 変数が再代入される可能性がない場合は常に
constを使用する。 - これにより、意図しない再代入を防ぎ、コードの意図を明確に表現できる。
- 多くのスタイルガイド (Airbnb、Google等) でも
constの優先使用が推奨されている。
- 変数が再代入される可能性がない場合は常に
- 再代入が必要な場合のみ、
letを使用する- カウンタ変数、条件によって値が変わる変数等、再代入が必要な場合にのみ
letを使用する。
- カウンタ変数、条件によって値が変わる変数等、再代入が必要な場合にのみ
varは使用しない。- 新規開発では
varを使用しない。 - レガシーコードや古い環境向けのコードを読む際の知識として理解しておく。
- 新規開発では
推奨される使用例を以下に示す。
// const をデフォルトとして使用
const PI = 3.14159;
const API_URL = "https://api.example.com";
const config = {
debug: true,
timeout: 5000
};
// 再代入が必要な場面でletを使用
let count = 0;
while (count < 10) {
count++;
}
let result;
if (someCondition) {
result = "value1";
} else {
result = "value2";
}
// ループのカウンタは let を使用
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
ESLintを使用している場合、no-var ルールで var の使用を禁止し、prefer-const ルールで const の優先使用を自動的に検出することができる。
関連情報
- JavaScriptの基礎 - プリミティブ型
- 7つのプリミティブ型、typeof演算子、型の自動変換
- JavaScriptの基礎 - 数値と算術演算子
- Number型の特性、算術演算子、Mathオブジェクト、BigInt
- JavaScriptの基礎 - 文字列
- テンプレートリテラル、文字列メソッド、タグ付きテンプレートリテラル
- JavaScriptの基礎 - 比較演算子と論理演算子
- ===/==の違い、短絡評価、Null合体演算子、オプショナルチェーン