「変数」とは何か
一旦、var と const は忘れて、let だけで考えます。
let count = 1;
console.log(count); // 1
count = count + 1;
console.log(count); // 2let でやっていることはシンプルで、
- 「名前(count)に値(1)を紐づける」
- 後から別の値を代入し直せる
というだけです。
ここから先の説明は、
var・let・const はすべて『名前と値を紐づける』という意味では同じ。
ただし、ルールと挙動が違う。
という視点で見ていくと整理しやすいです。
var / let / const の大きな違い
結論から表でざっくりまとめます。
| キーワード | 再代入 | 再宣言 | スコープ | Hoisting 時の挙動 |
|---|---|---|---|---|
var | できる | できる | 関数スコープ | 宣言が巻き上がり、初期値は undefined |
let | できる | できない | ブロックスコープ | 巻き上がるが TDZ に入り、宣言前参照はエラー |
const | できない | できない | ブロックスコープ | let と同じく TDZ、宣言時に必ず初期化 |
一つずつ見ていきます。
再代入の有無
let x = 1;
x = 2; // OK
const y = 1;
y = 2; // エラー(TypeError)let:後から値を変えられるconst:同じ名前に別の値を入れ直すことはできない
※ 後で説明しますが、const でオブジェクトを束縛した場合、
「オブジェクトの中身」は変えられます。
ここでの「再代入」は “別の値に束縛し直す” という意味です。
再宣言の可否
var a = 1;
var a = 2; // OK(上書きされる)
let b = 1;
// let b = 2; // エラー(Identifier 'b' has already been declared)
const c = 1;
// const c = 2; // 同上エラーvar は同じスコープ内で何度でも宣言できてしまうので、タイポなどがエラーになりにくいです。(≒バグに気づきにくい)
let / const は「同じ名前を同じスコープで二度宣言しようとするとエラーになります」
→ こちらの方が安全と言えます
スコープ:var は「関数」、let / const は「ブロック」
var は関数スコープ
if (true) {
var x = 10;
}
console.log(x); // 10(ブロックの外でも見える)var で宣言した変数は、関数全体がスコープになります。
function sample() {
if (true) {
var v = 123;
}
console.log(v); // 123
}
sample();
// console.log(v); // ここは関数の外なのでエラーvは「sample 関数の中ならどこからでも見える」- 逆に言うと、
ifブロックではスコープは区切られていない
let / const はブロックスコープ
if (true) {
let x = 10;
const y = 20;
}
// console.log(x); // ReferenceError
// console.log(y); // ReferenceErrorlet / const は {} で囲まれたブロックの中だけ有効です。
function sample() {
if (true) {
let v = 123;
}
// console.log(v); // エラー
}- 関数内でも、「
ifの{}の内側だけ」というより細かいスコープを作れる - これが
let/constを推奨する大きな理由の一つです
グローバル / 関数 / ブロック の3レベル
スコープのレベル感を整理しておきます。
- グローバルスコープ
- どこからでも参照できる領域(モジュールでは少し意味が変わる)
- 関数スコープ
- 関数の中だけ有効
- ブロックスコープ
{}の中だけ有効(if,for, 単純な{}など)
const globalValue = "global"; // グローバル
function func() { // ← 関数スコープ開始
const inFunc = "func内";
if (true) { // ← ブロックスコープ開始
const inBlock = "ブロック内";
console.log(globalValue); // OK
console.log(inFunc); // OK
console.log(inBlock); // OK
}
console.log(globalValue); // OK
console.log(inFunc); // OK
// console.log(inBlock); // エラー
}
func();
// console.log(inFunc); // エラー
// console.log(inBlock); // エラー- 外側から内側は見えない
- 内側から外側は見える
という 「入れ子構造」 をイメージしておくと、この後のクロージャやthisの理解が楽になります。
Hoisting(巻き上げ)と TDZ(Temporal Dead Zone)
var の巻き上げ
var には典型的な「巻き上げ」の挙動があります。
console.log(x); // undefined(エラーではない)
var x = 10;
console.log(x); // 10内部的には次のように扱われていると思ってください。
var x; // 宣言だけ先にされる(初期値は undefined)
console.log(x); // undefined
x = 10; // ここで代入
console.log(x); // 10- 宣言だけがスコープの先頭に「巻き上げられる」
- 初期値は自動的に
undefinedで埋められる
これが var がバグを生みやすい大きな原因です。
let / const と TDZ
let / const も宣言自体は巻き上がりますが、「初期化されるまでのあいだ」が TDZ(Temporal Dead Zone) と呼ばれる状態になり、その間に変数を触るとエラーになります。
console.log(x); // ReferenceError
let x = 10;
console.log(x); // 10内部イメージ:
// let x; // ★宣言はスコープの先頭に存在するが、まだ初期化されていない(TDZ)
console.log(x); // ここで触ると「初期化前だからダメ」と怒られる
x = 10; // ここで初めて初期化される
console.log(x); // 10const も同じく TDZ がある上に、「宣言と同時に初期化が必須」です。
const y; // SyntaxError(初期値が必須)
const z = 10; // OKstrict mode との関係
var- strict でも non-strict でも「巻き上げ+初期値
undefined」の挙動は同じ - ただし、
x = 10;と どこにもvar xがない場合は strict ではエラー
- strict でも non-strict でも「巻き上げ+初期値
let/const- strict / non-strict に関係なく、TDZ で「宣言前参照は ReferenceError」
モダンコードでは基本 strict mode + let / const 前提なので、
「宣言前に変数を触るとエラーになる」という挙動に慣れておくとよいです。
const とオブジェクト
const は「再代入ができない」だけであって、
オブジェクトの中身が不変(イミュータブル)になるわけではありません。
配列の場合
const nums = [1, 2, 3];
nums.push(4); // OK(配列の中身を変更)
console.log(nums); // [1, 2, 3, 4]
// nums = [1, 2]; // これはエラー(別の配列を入れ直そうとしている)numsという「名前 → 配列オブジェクト」の紐付けは変えられない- しかし、紐付けた先のオブジェクトの内容(配列の中身)は操作できる
オブジェクトの場合
const user = {
name: "Taro",
age: 20,
};
user.age = 21; // OK
user.country = "Japan"; // OK
console.log(user);
// { name: "Taro", age: 21, country: "Japan" }
// user = {}; // これはエラー(新しいオブジェクトを代入しようとしている)constは「参照先を変えない」- 中身を変えたくない場合は
Object.freezeなどを使う
と理解しておくと、挙動に納得がいきやすくなります。
実務でのおすすめルール
実務では、次のようなシンプルなルールにしているチームが多いです。
- デフォルトは
constを使う- 「後から値を変える必要がある」と判断したときだけ
letにする
- 「後から値を変える必要がある」と判断したときだけ
varは使わない- 古いコードを読むときに挙動を理解できれば十分
- 新しいコードでは ESLint で
varを禁止していることが多い
- スコープを意識する
- 「どこからどこまで見えてほしい変数か」を考える
→ なるべく 狭いスコープ(ブロックの中) で宣言する
- 「どこからどこまで見えてほしい変数か」を考える
この講座でも、以降のサンプルは基本的に const / let で書きます。
>>次へ(執筆中)
コメント