- JavaScript
JavaScript var・let・constの違い|スコープと巻き上げを比較表で
var・let・const はどれも JavaScript の変数宣言で、違いは「再宣言できるか」「再代入できるか」「どこまで有効か(スコープ)」「宣言の前に触れたときの動き(巻き上げ)」の 4 点にあります。 結論から言うと、基本は const、値を入れ直す必要があるときだけ let、var は新しく書くコードでは使わない、という使い分けでほぼすべての場面をカバーできます。
この記事では、3 つの違いを比較表にまとめたうえで、各エラーの再現、for ループと setTimeout の出力差、const とオブジェクト、関数・class の巻き上げ、switch 文のスコープ、ESLint のルールまでを、手元で実行した結果つきで紹介します。検証は Node.js 22.22.2 と ESLint 10.11.0 で行いました。出力中のパスは /home/user/demo に置き換え、スタックトレースは先頭だけを載せています。
var・let・constの早見表
| 項目 | var |
let |
const |
|---|---|---|---|
| 再宣言(同じスコープで同じ名前) | できる | SyntaxError | SyntaxError |
| 再代入 | できる | できる | TypeError |
| 宣言時の初期値 | 省略できる(undefined) |
省略できる(undefined) |
必須 |
| スコープ | 関数スコープ | ブロックスコープ | ブロックスコープ |
| 巻き上げ(宣言前に参照) | undefined が返る |
ReferenceError(TDZ) | ReferenceError(TDZ) |
| グローバルオブジェクトへの登録 | される(スクリプトのトップレベル) | されない | されない |
| for ループでの束縛 | ループ全体で 1 つ | 周回ごとに新しく作られる | 周回ごとに新しく作られる(for...of など) |
| 導入された版 | 最初から | ES2015(ES6) | ES2015(ES6) |
| おすすめの用途 | 既存コードを読むための知識 | 値を入れ直す変数 | それ以外すべて(基本) |
出てくるエラー文の対応表です。
| エラー文 | 起きる場面 |
|---|---|
SyntaxError: Identifier 'x' has already been declared |
let・const で同じ名前を再宣言した |
TypeError: Assignment to constant variable. |
const の変数に再代入した |
SyntaxError: Missing initializer in const declaration |
const を初期値なしで宣言した |
ReferenceError: Cannot access 'x' before initialization |
let・const・class を宣言の前に使った(TDZ) |
ReferenceError: x is not defined |
そのスコープに、その名前の変数がない |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScript var・let・constの違い|スコープと巻き上げを比較表で」
再宣言と再代入の違い
「再宣言」は同じ名前でもう一度 var x や let x と書くこと、「再代入」は宣言済みの変数に x = 新しい値 と入れ直すことです。
var:再宣言も再代入もできる
var count = 1;
var count = 2; // 再宣言できる
count = 3; // 再代入できる
console.log(count);
3
エラーにならないので一見便利ですが、長い関数の中で同じ名前をうっかり 2 回宣言しても気付けない、という面があります。
let:再宣言は SyntaxError
let count = 1;
let count = 2;
/home/user/demo/redeclare_let.js:2
let count = 2;
^
SyntaxError: Identifier 'count' has already been declared
SyntaxError は、コードを実行する前の構文解析の段階で出ます。 つまり、ファイルの 1 行目すら実行されません。var total = 1; のあとに let total = 2; と書いた場合も、同じエラーになりました。名前の重複を、実行前に知らせてくれる仕組みです。
const:再代入は TypeError
const TAX = 0.1;
TAX = 0.08;
/home/user/demo/const_assign.js:2
TAX = 0.08;
^
TypeError: Assignment to constant variable.
こちらは実行時のエラー(TypeError)です。また、const は宣言と同時に値を入れる必要があり、const TAX; とだけ書くと次のエラーになります。
SyntaxError: Missing initializer in const declaration
| 操作 | var |
let |
const |
|---|---|---|---|
x = 2(再代入) |
できる | できる | TypeError: Assignment to constant variable. |
| 同じスコープで再宣言 | できる | SyntaxError: ... already been declared |
同左 |
| 初期値なしの宣言 | できる | できる | SyntaxError: Missing initializer |
スコープの違い
スコープとは、その変数を使える範囲のことです。 var は「関数」で区切られ、let と const は「ブロック({ })」で区切られます。
| スコープの種類 | 区切り | 対象 |
|---|---|---|
| グローバルスコープ | どこにも囲まれていない場所 | すべて |
| モジュールスコープ | ES モジュール・CommonJS の 1 ファイル | すべて |
| 関数スコープ | function の { } |
var・let・const |
| ブロックスコープ | if・for・while・switch・単独の { } |
let・const だけ |
if ブロックの中で 3 種類を宣言して確かめます。
function demo() {
if (true) {
var a = "var";
let b = "let";
const c = "const";
}
console.log(a);
try { console.log(b); } catch (e) { console.log(e.name + ": " + e.message); }
try { console.log(c); } catch (e) { console.log(e.name + ": " + e.message); }
}
demo();
try { console.log(a); } catch (e) { console.log(e.name + ": " + e.message); }
for (var i = 0; i < 3; i++) {}
console.log("i =", i);
for (let j = 0; j < 3; j++) {}
console.log(typeof j);
実行結果:
var
ReferenceError: b is not defined
ReferenceError: c is not defined
ReferenceError: a is not defined
i = 3
undefined
| 行 | 結果 | 読み取れること |
|---|---|---|
console.log(a)(if の外・関数の中) |
var |
var は if のブロックを越えて、関数全体で使える |
console.log(b)・console.log(c) |
ReferenceError | let・const は if の { } の中だけ |
関数の外の console.log(a) |
ReferenceError | var も関数の外には出ない |
ループ後の i |
3 |
var のループ変数はループの外に残る |
ループ後の typeof j |
undefined |
let のループ変数はループの中だけ |
変数の有効範囲が狭いほど、「この変数はどこで変わるのか」を追う範囲も狭くなります。 let と const がブロックスコープであることは、コードを読みやすくするための性質です。
宣言なしの代入はグローバル変数になる
var・let・const のどれも付けずに代入すると、非 strict モードではグローバル変数が作られます。strict モードや ES モジュールでは ReferenceError になり、書き忘れに気付けます。
function sloppy() { leaked = 1; }
sloppy();
console.log(globalThis.leaked);
(function () {
"use strict";
try { leaked2 = 1; } catch (e) { console.log(e.name + ": " + e.message); }
})();
1
ReferenceError: leaked2 is not defined
ES モジュール(.mjs や type="module")は自動的に strict モードで動きます。
巻き上げ(ホイスティング)と TDZ
巻き上げとは、変数や関数の宣言が、書いた位置に関係なくスコープの先頭で行われたかのように扱われる動きです。 var・let・const のどれも巻き上げられますが、宣言の前に触れたときの結果が異なります。
console.log(a); // undefined
var a = 1;
try {
console.log(b);
} catch (e) {
console.log(e.name + ": " + e.message);
}
let b = 2;
try {
console.log(c);
} catch (e) {
console.log(e.name + ": " + e.message);
}
const c = 3;
try {
console.log(d);
} catch (e) {
console.log(e.name + ": " + e.message);
}
undefined
ReferenceError: Cannot access 'b' before initialization
ReferenceError: Cannot access 'c' before initialization
ReferenceError: d is not defined
| 変数 | 宣言前に参照した結果 | 理由 |
|---|---|---|
var a |
undefined |
宣言が巻き上げられ、undefined で初期化される。代入(= 1)は元の位置で行われる |
let b・const c |
Cannot access 'b' before initialization |
宣言は巻き上げられるが、初期化されていない |
d(宣言なし) |
d is not defined |
そもそも存在しない |
before initialization と is not defined はメッセージが違う点に注目してください。前者は「変数があることは知っているが、まだ使える状態になっていない」という意味で、let と const も巻き上げられている証拠です。
TDZ(一時的なデッドゾーン)とは
TDZ(Temporal Dead Zone)は、スコープの先頭から、let・const の宣言の行が実行されるまでの区間です。この区間でその変数に触れると ReferenceError になります。
let x = "outer";
{
// ここからブロック内の x の TDZ
try {
console.log(x);
} catch (e) {
console.log(e.name + ": " + e.message);
}
let x = "inner"; // ここで TDZ が終わる
console.log(x);
}
ReferenceError: Cannot access 'x' before initialization
inner
外側に同じ名前の x があっても、ブロックの中の console.log(x) は外側を見に行きません。ブロック内の let x が先頭まで巻き上げられ、外側の x を隠しているからです。もし巻き上げがなければ、ここでは outer が表示されるはずです。
TDZ の細かい動きも確認しました。
try { console.log(typeof notDeclared); console.log(typeof y); } catch (e) { console.log(e.name + ": " + e.message); }
let y = 1;
function f(a = b, b = 1) { return [a, b]; }
try { f(); } catch (e) { console.log(e.name + ": " + e.message); }
undefined
ReferenceError: Cannot access 'y' before initialization
ReferenceError: Cannot access 'b' before initialization
| 場面 | 結果 |
|---|---|
宣言していない名前に typeof |
"undefined"(エラーにならない) |
TDZ の中の変数に typeof |
ReferenceError |
| 引数の既定値で、後ろの引数を参照 | ReferenceError(引数も左から順に初期化される) |
TDZ は制約のように見えますが、「宣言より前で使っている」という書き間違いを、undefined のまま進めずにその場で知らせてくれる仕組みです。var の undefined は、あとで NaN や Cannot read properties of undefined といった離れた場所のエラーとして現れるため、原因までさかのぼる手間がかかります。
グローバルオブジェクトへの登録
ブラウザの <script>(モジュールではない通常のスクリプト)のトップレベルで var を宣言すると、グローバルオブジェクト(ブラウザでは window、共通の名前は globalThis)のプロパティとして登録されます。 let・const・class は登録されません。
Node.js では、vm.runInThisContext を使うとブラウザの通常のスクリプトと同じ扱いで実行できるので、これで確かめました。
const vm = require("node:vm");
// ブラウザの <script> と同じ「スクリプト」として実行する
vm.runInThisContext(`
var gVar = "var";
let gLet = "let";
const gConst = "const";
function gFunc() {}
class GClass {}
`);
console.log(globalThis.gVar, globalThis.gLet, globalThis.gConst);
console.log(typeof globalThis.gFunc, typeof globalThis.GClass);
console.log(vm.runInThisContext("gLet + ' / ' + gConst + ' / ' + typeof GClass"));
// このファイル自体は CommonJS モジュールなので、トップレベルの var もグローバルにならない
var inModule = 1;
console.log(globalThis.inModule);
var undefined undefined
function undefined
let / const / function
undefined
| 宣言(スクリプトのトップレベル) | globalThis.名前 |
別のスクリプトから名前で参照 |
|---|---|---|
var・関数宣言 |
登録される | できる |
let・const・class |
登録されない(undefined) |
できる(グローバルの字句環境に置かれる) |
| モジュール内のすべての宣言 | 登録されない | できない(そのファイルの中だけ) |
3 行目の出力のとおり、let・const もスクリプト間では共有されますが、window.gLet のようにプロパティとしては見えません。var で name や status のような名前を宣言すると、window.name など既存のプロパティと重なって想定外の値になることがあります。let・const、そして ES モジュールを使えば、この重なりを避けられます。
for ループと setTimeout:3,3,3 と 0,1,2
var と let の違いがいちばんはっきり出る例です。
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 0);
}
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2
理由は、変数がいくつ作られるかの違いです。
| 宣言 | 変数の数 | コールバックが見る値 |
|---|---|---|
var i |
ループ全体で 1 つ | 3 つのコールバックが同じ i を共有。実行される頃にはループが終わり 3 |
let j |
周回ごとに新しい j が作られる |
それぞれの周回の値(0、1、2)を別々に覚えている |
setTimeout のコールバックは、ループがすべて終わったあとに実行されます。var の i は 1 つしかないので、その時点の値である 3 が 3 回表示されます。let は周回ごとに新しい変数が作られ、前の周回の値がコピーされたうえで j++ が行われるため、各コールバックが自分の周回の値を覚えています。
クリックイベントの登録や、非同期処理をループで開始するときにも同じことが起こります。ループ変数を let にするだけで、意図どおりの値になります。
var のまま 0,1,2 にする書き方と、const が使えるループ
既存のコードを読むときのために、let が登場する前の書き方も確認しました。
// var のまま 0,1,2 にする方法(参考)
for (var i = 0; i < 3; i++) {
(function (n) {
setTimeout(() => console.log("IIFE:", n), 0);
})(i);
}
for (var k = 0; k < 3; k++) {
setTimeout((n) => console.log("引数:", n), 0, k);
}
// for...of / for...in では const が使える
for (const v of [10, 20, 30]) {
setTimeout(() => console.log("for-of const:", v), 0);
}
try {
for (const m = 0; m < 3; m++) {}
} catch (e) {
console.log(e.name + ": " + e.message);
}
TypeError: Assignment to constant variable.
IIFE: 0
IIFE: 1
IIFE: 2
引数: 0
引数: 1
引数: 2
for-of const: 10
for-of const: 20
for-of const: 30
| ループの種類 | おすすめの宣言 | 理由 |
|---|---|---|
for (let i = 0; i < n; i++) |
let |
i++ で再代入するため。const では TypeError |
for (const v of 配列) |
const |
周回ごとに新しい変数が作られ、再代入しない |
for (const key in オブジェクト) |
const |
同上 |
配列.forEach((v, i) => ...) |
宣言不要 | 引数が周回ごとに渡される |
const のオブジェクトは変更できる:Object.freeze
const が禁止するのは「変数への再代入」であって、「値の変更」ではありません。 const で宣言したオブジェクトや配列の中身は、そのまま書き換えられます。
"use strict";
const user = { name: "佐藤", tags: ["a"], address: { city: "東京" } };
user.name = "鈴木"; // プロパティの変更はできる
user.tags.push("b");
console.log(user);
try { user = {}; } catch (e) { console.log(e.name + ": " + e.message); }
{ name: '鈴木', tags: [ 'a', 'b' ], address: { city: '東京' } }
TypeError: Assignment to constant variable.
変数は「値そのもの」ではなく「オブジェクトへの参照」を持っています。const は、その参照を別のものに付け替えることを禁止しているだけです。
Object.freeze で中身も固定する(ただし浅い)
中身の変更も止めたいときは Object.freeze を使います。
const frozen = Object.freeze({ name: "佐藤", address: { city: "東京" } });
try { frozen.name = "鈴木"; } catch (e) { console.log(e.name + ": " + e.message); }
try { frozen.age = 20; } catch (e) { console.log(e.name + ": " + e.message); }
try { delete frozen.name; } catch (e) { console.log(e.name + ": " + e.message); }
frozen.address.city = "大阪"; // 入れ子は凍結されていない(浅い)
console.log(frozen, Object.isFrozen(frozen), Object.isFrozen(frozen.address));
TypeError: Cannot assign to read only property 'name' of object '#<Object>'
TypeError: Cannot add property age, object is not extensible
TypeError: Cannot delete property 'name' of #<Object>
{ name: '佐藤', address: { city: '大阪' } } true false
変更・追加・削除はすべて止まりましたが、入れ子の address.city は書き換えられました。 Object.freeze が固定するのは 1 階層目だけ(浅い凍結)です。
もう 1 つの注意点は、strict モードでないと、エラーにならず黙って無視されることです。先頭の "use strict" を外したファイルで試すと、次のようになりました。
const frozen = Object.freeze({ name: "佐藤" });
frozen.name = "鈴木"; // 非 strict ではエラーにならず、黙って無視される
console.log(frozen.name);
佐藤
入れ子まで固定する deepFreeze
入れ子も含めて固定するには、再帰的に freeze する関数を用意します。
function deepFreeze(obj) {
for (const value of Object.values(obj)) {
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
deepFreeze(value);
}
}
return Object.freeze(obj);
}
const deep = deepFreeze({ name: "佐藤", address: { city: "東京" }, tags: ["a"] });
try { deep.address.city = "大阪"; } catch (e) { console.log(e.name + ": " + e.message); }
try { deep.tags.push("b"); } catch (e) { console.log(e.name + ": " + e.message); }
const copy = structuredClone(deep);
copy.address.city = "名古屋";
console.log(copy.address.city, deep.address.city);
TypeError: Cannot assign to read only property 'city' of object '#<Object>'
TypeError: Cannot add property 1, object is not extensible
名古屋 東京
凍結したオブジェクトを元に変更版を作りたいときは、structuredClone やスプレッド構文({ ...deep, name: "鈴木" })でコピーを作ります。コピーは凍結されていないので、自由に変更できます。
| 手段 | 再代入 | プロパティの変更 | 入れ子の変更 |
|---|---|---|---|
let obj = {...} |
できる | できる | できる |
const obj = {...} |
TypeError | できる | できる |
const obj = Object.freeze({...}) |
TypeError | 止まる | できる |
const obj = deepFreeze({...}) |
TypeError | 止まる | 止まる |
TypeScript の as const・readonly |
コンパイル時に検出 | コンパイル時に検出 | as const は入れ子も対象 |
設定値やマスターデータのように、どこからも書き換えられたくないオブジェクトには freeze が役立ちます。
関数宣言・関数式・class の巻き上げ
巻き上げは変数だけの話ではありません。関数宣言は中身ごと巻き上げられ、関数式と class は、代入先の変数のルールに従います。
console.log(decl()); // 関数宣言:呼べる
function decl() { return "関数宣言"; }
console.log(typeof exprVar); // var の関数式:undefined
try { exprVar(); } catch (e) { console.log(e.name + ": " + e.message); }
var exprVar = function () { return "関数式(var)"; };
try { exprConst(); } catch (e) { console.log(e.name + ": " + e.message); }
const exprConst = () => "アロー関数(const)";
try { new Item(); } catch (e) { console.log(e.name + ": " + e.message); }
class Item {}
console.log(exprVar(), exprConst(), new Item() instanceof Item);
関数宣言
undefined
TypeError: exprVar is not a function
ReferenceError: Cannot access 'exprConst' before initialization
ReferenceError: Cannot access 'Item' before initialization
関数式(var) アロー関数(const) true
| 書き方 | 宣言前に使うと | 理由 |
|---|---|---|
function decl() {}(関数宣言) |
呼べる | 関数の中身ごと巻き上げられる |
var f = function () {} |
TypeError: f is not a function |
var の巻き上げで undefined。それを呼ぼうとした |
const f = () => {}・let f = ... |
ReferenceError: Cannot access ... before initialization |
TDZ |
class Item {} |
ReferenceError: Cannot access ... before initialization |
class も let・const と同じく TDZ がある |
関数宣言の巻き上げを使うと、「ファイルの上に処理の流れ、下に細かい関数」という並べ方ができます。一方、const のアロー関数は定義より後でしか使えないので、上から下へ読む順番と実行の順番がそろいます。どちらの書き方を選ぶかは、チームで統一すると読みやすくなります。
switch 文の case とスコープ
意外と見落としやすいのが switch 文です。switch は { } 全体で 1 つのブロックで、case ごとには区切られていません。
function label(kind) {
switch (kind) {
case "a":
let msg = "A です";
return msg;
case "b":
let msg = "B です";
return msg;
}
}
/home/user/demo/switch1.js:7
let msg = "B です";
^
SyntaxError: Identifier 'msg' has already been declared
2 つの case が同じブロックに属しているため、再宣言の扱いになります。では、2 つ目を再代入に変えるとどうでしょうか。
function label(kind) {
switch (kind) {
case "a":
let msg = "A です";
return msg;
case "b":
msg = "B です"; // 同じブロックだが、let の行を通っていない
return msg;
}
}
console.log(label("a"));
try { console.log(label("b")); } catch (e) { console.log(e.name + ": " + e.message); }
A です
ReferenceError: Cannot access 'msg' before initialization
"b" のときは let msg の行を実行せずに case "b" へ飛ぶため、msg は TDZ のままになり、ReferenceError になります。
解決は、case ごとに { } で囲んでブロックを作ることです。
function label(kind) {
switch (kind) {
case "a": {
const msg = "A です";
return msg;
}
case "b": {
const msg = "B です";
return msg;
}
default: {
const msg = "その他";
return msg;
}
}
}
console.log(label("a"), label("b"), label("c"));
A です B です その他
| 直す前 | 直した後 |
|---|---|
case "a": let msg = ...; |
case "a": { const msg = ...; } |
| case をまたいで同じ変数名を宣言 | case ごとのブロックで、それぞれ宣言 |
ESLint の no-case-declarations ルール(推奨設定に含まれます)が、囲み忘れを検出してくれます。
使い分けの指針:基本 const、必要なら let
| 場面 | 選ぶ宣言 | 理由 |
|---|---|---|
| 値を入れ直さない変数(大半はこれ) | const |
「この変数は変わらない」と読み手に伝わる |
| オブジェクト・配列・関数 | const |
中身を変更しても、変数自体は入れ直さない |
| カウンター、合計値、条件で値が変わる変数 | let |
再代入が必要 |
for (let i = 0; ...) のループ変数 |
let |
i++ で再代入する |
for...of・for...in のループ変数 |
const |
周回ごとに新しく作られる |
| あとで値を入れる変数(try の外で宣言など) | let |
const は初期値が必須 |
| 新しく書くコード全般 | var は使わない | 関数スコープ・再宣言・undefined の巻き上げを避けられる |
const を基本にすると、let が出てきたときに「この変数は途中で変わる」と気付けるようになります。変わる変数だけが目立つので、処理を追うときの注意を絞れます。
let を const に寄せる書き換えの例です。
// 直す前:あとから代入するために let
let label;
if (stock > 0) {
label = "在庫あり";
} else {
label = "入荷待ち";
}
// 直した後:式にまとめて const
const label = stock > 0 ? "在庫あり" : "入荷待ち";
条件が増える場合は、値を返す小さな関数に切り出すと const で受け取れます。
既存の var を書き換えるときの確認点
var を let・const に置き換えるときは、次の 3 点を確認すると安全に進められます。
| 確認点 | 理由 |
|---|---|
| ブロックの外でその変数を使っていないか | let・const はブロックの外から見えなくなる |
| 宣言より前で使っていないか | undefined だったものが ReferenceError になる |
| 同じ名前を 2 回宣言していないか | let・const では SyntaxError になる |
ESLint の --fix に任せると、機械的に置き換えられる箇所だけが書き換わります。
ESLint で自動チェックする:prefer-const と no-var
使い分けは、ESLint のルールで自動的にそろえられます。ESLint 10.11.0 で確認しました。
| ルール | 内容 | 推奨設定(js.configs.recommended) |
自動修正 |
|---|---|---|---|
no-var |
var を見つけたら報告 | 含まれない(自分で追加) | あり |
prefer-const |
再代入されていない let を報告 | 含まれない(自分で追加) | あり |
no-case-declarations |
case 内の let・const・class・function を報告 | 含まれる | なし |
no-const-assign |
const への再代入を報告 | 含まれる | なし |
no-redeclare |
同じ名前の再宣言を報告 | 含まれる | なし |
no-var と prefer-const は推奨設定に含まれていない(手元の ESLint 10.11.0 で設定の中身を確認)ので、設定ファイルに追加します。
npm i -D eslint @eslint/js
eslint.config.mjs:
import js from "@eslint/js";
export default [
js.configs.recommended,
{
rules: {
"no-var": "error",
"prefer-const": "error",
},
},
];
チェック対象の sample.js:
var total = 0;
let rate = 0.1;
let count = 0;
count = count + 1;
function label(kind) {
switch (kind) {
case "a":
let msg = "A";
return msg;
default:
return "other";
}
}
export { total, rate, count, label };
npx eslint sample.js
/home/user/demo/sample.js
1:1 error Unexpected var, use let or const instead no-var
2:5 error 'rate' is never reassigned. Use 'const' instead prefer-const
9:7 error Unexpected lexical declaration in case block no-case-declarations
9:11 error 'msg' is never reassigned. Use 'const' instead prefer-const
✖ 4 problems (4 errors, 0 warnings)
3 errors and 0 warnings potentially fixable with the `--fix` option.
npx eslint --fix を実行すると、3 件が自動で書き換わりました。
const total = 0;
const rate = 0.1;
let count = 0;
count = count + 1;
再代入のある count は let のまま残り、再代入のない total と rate は const になっています。 残った no-case-declarations は、前の章のとおり case を { } で囲んで直します。設定ファイルの書き方の全体像は、別記事「ESLint flat config設定」で扱っています。
よくある質問
Q. let と const はどちらが速いですか? A. 現在の JavaScript エンジンでは、実用上の速度差を気にする必要はありません。選ぶ基準は速度ではなく、「再代入するかどうかを読み手に伝える」ことです。再代入しないなら const、するなら let と決めておけば十分です。
Q. const なのに配列に push できるのはなぜですか?
A. const が禁止するのは変数への再代入(arr = [...])で、配列の中身の変更(arr.push(...))は対象外だからです。中身も固定したいときは Object.freeze(arr) を使います。freeze は 1 階層目だけが対象なので、入れ子まで固定するには再帰的に freeze します。
Q. var は今後使えなくなりますか? A. なくなる予定はありません。JavaScript は過去のコードが動き続けることを大切にしており、var も仕様に残ります。既存のコードを読むために挙動を知っておき、新しく書くコードでは let と const を使う、という付き合い方がおすすめです。
Q. ブラウザのコンソールでは let を再宣言できるのはなぜですか? A. Chrome などの開発者ツールのコンソールは、試し書きをしやすくするために、入力ごとの let・const の再宣言を特別に許可しています。ファイルに書いたスクリプトでは、この記事のとおり SyntaxError になります(コンソールの挙動は手元では実行しておらず、Chrome DevTools の公式ドキュメントで確認した内容です)。
まとめ
- 基本は const、再代入が必要なときだけ let、新しいコードで var は使わない
- var は関数スコープ、let・const はブロックスコープ。if や for の
{ }の外からは見えない - 3 つとも巻き上げられるが、var は
undefined、let・const は TDZ によりCannot access 'x' before initialization - for ループ + setTimeout は var で 3,3,3、let で 0,1,2。let は周回ごとに新しい変数が作られる
- const は再代入を止めるだけ。中身も固定するなら
Object.freeze(浅い)、入れ子までなら再帰的に freeze - 関数宣言は中身ごと巻き上げ、関数式と class は変数のルールに従う。switch の case は
{ }で囲む - ESLint の
no-varとprefer-constを追加すると、--fixで自動的にそろえられる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScript var・let・constの違い|スコープと巻き上げを比較表で」
株式会社RJCでは、JavaScript の変数宣言やスコープの考え方を、仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。