導入
JavaScriptでは、変数を宣言するためにvar、let、constを使用できます。
現在ではletやconstを使うことが一般的ですが、なぜlet、constが導入されたのでしょうか。
本記事では、
-
varにはどのような問題があるのか - その問題によって何が困るのか
-
let、constはどのように解決したのか - 結果として何が解決されたのか
という順番で整理します。
varの問題点とlet、constを導入したことによる解決
varの問題点①:ブロックスコープを持たない
(1) varの問題点
varはブロックスコープを持たず、関数スコープを持ちます。
if (true) {
var value = "hello";
}
console.log(value);
// hello
ifのブロックを抜けた後でもvalueを参照できます。
(2) 何が困るのか
ブロック内だけで使用するつもりだった変数が、ブロックの外からも参照できてしまいます。
例えば、
function process() {
if (someCondition) {
var result = "changed";
}
console.log(result);
}
のようなコードでは、ifの中のresultはifブロック専用の変数ではありません。
関数スコープに存在するresultを再利用しています。
コードが大きくなると、変数の有効範囲を把握しにくくなります。
また、ブロック内だけで使用するつもりだった変数が、ブロックの外に影響を与える可能性もあります。
(3) let、constによる解決
letとconstはブロックスコープを持ちます。
if (true) {
let value = "hello";
}
console.log(value);
// ReferenceError
constも同様です。
if (true) {
const value = "hello";
}
console.log(value);
// ReferenceError
(4) 何が解決されたのか
let、constによって、変数の有効範囲をブロック単位まで狭められるようになりました。
これによって、
- ブロック内だけで使用する変数を明確にできる
- 意図しない変数の共有を防げる
- 変数の有効範囲を小さくできる
- 変数名が他のコードに与える影響を小さくできる
ようになりました。
つまり、varの「ブロックによってスコープを分離できない」という問題が解決されました。
varの問題点②:同じスコープで再宣言できる
(1) varの問題点
varは、同じスコープで同じ変数名を再宣言できます。
var value = 1;
var value = 2;
console.log(value);
// 2
再宣言してもエラーになりません。
(2) 何が困るのか
変数名の重複を誤って記述しても、エラーとして検出できません。
var userName = "Alice";
// 多くのコード
var userName = "Bob";
この2回目の宣言が意図したものなのか、単なる記述ミスなのかを判断することが難しくなります。
コードが大きくなるほど、再宣言による意図しない上書きが問題になりやすくなります。
(3) let、constによる解決
letとconstでは、同じスコープで同じ名前を再宣言できません。
let value = 1;
let value = 2;
// SyntaxError
const value = 1;
const value = 2;
// SyntaxError
(4) 何が解決されたのか
同じスコープで変数を二重に宣言してしまった場合、エラーとして検出できるようになりました。
これによって、意図しない変数の再宣言を防ぎやすくなりました。
varの問題点③:宣言前のアクセスがエラーにならない
(1) varの問題点
varでは、変数を宣言する前にアクセスしてもエラーにならず、undefinedが返ります。
console.log(value);
var value = 10;
// undefined
これはvarのホイスティング(巻き上げ)によるものです。
概念的には、次のように考えると分かりやすいでしょう。
var value;
console.log(value);
value = 10;
(2) 何が困るのか
コードを上から読むと、console.log(value)の時点ではまだvalueに値を代入していません。
それでもエラーにならないため、「まだ使用すべきではない変数を使用した」というミスに気づきにくくなります。
コードが大きくなると、宣言位置と使用位置が離れてしまい、問題が分かりにくくなります。
(3) let、constによる解決
let、constでは、宣言前に変数へアクセスするとエラーになります。
console.log(value);
let value = 10;
// ReferenceError
console.log(value);
const value = 10;
// ReferenceError
この仕組みにはTemporal Dead Zone(TDZ)が関係しています。
TDZとは、let、constで宣言された変数が、スコープ内に存在していても、初期化されるまでアクセスできない期間のことです。
そのため、初期化される前の変数にアクセスするとReferenceErrorになります。
(4) 何が解決されたのか
let、constでは、変数が初期化される前のアクセスがエラーになります。
そのため、
宣言
↓
初期化
↓
使用
という自然な順序でコードを書くことが求められます。
これによって、初期化前の変数使用を早期発見できるようになりました。
varの問題点④:再代入しない意図を表現できない
(1) varの問題点
varで宣言した変数は再代入できます。
var taxRate = 0.1;
taxRate = 0.2;
しかし、実際には一度決めた後に変更する必要がない値もあります。
varだけでは、「この値は再代入しない」という意図をコードから明確に表現できません。
(2) 何が困るのか
var taxRate = 0.1;
と書かれていても、後から値が変更されるのかどうかは、その変数を使用しているコード全体を確認しなければ分かりません。
(3) constによる解決
constを使用すると、変数への再代入を禁止できます。
const taxRate = 0.1;
taxRate = 0.2;
// TypeError
(4) 何が解決されたのか
constによって、「この変数には再代入しない」という意図をコードそのもので表現できるようになりました。
ただし、constはオブジェクトそのものをimmutableにするものではありません。
const user = {
name: "Alice"
};
user.name = "Bob"; // 可能
禁止されるのは変数そのものへの再代入である点は注意が必要です。
user = {}; // TypeError
varの問題点⑤:トップレベルのvarがグローバルオブジェクトと結びつく
(1) varの問題点
ブラウザの通常の<script>では、トップレベルで宣言したvarはグローバルオブジェクトのプロパティになります。
var value = 10;
console.log(window.value);
// 10
(2) 何が困るのか
グローバルオブジェクトに名前を追加することは、グローバルな名前空間を汚染することにつながります。
例えば、
var user = "Alice";
とすると、
window.user
という名前が作られます。
別のJavaScriptが同じ名前を使用すると、意図しない名前の衝突が発生する可能性があります。
(3) let、constによる解決
通常の<script>でトップレベルにlet、constを宣言しても、それらはwindowのプロパティにはなりません。
let value = 10;
const name = "Alice";
console.log(window.value);
// undefined
console.log(window.name);
// undefined
(4) 何が解決されたのか
let、constによって、トップレベルの変数をwindowのプロパティとして公開するというvarの問題は解消されました。
ただし、ここには重要な注意点があります。
windowのプロパティにならないことと、他のScriptから参照できないことは同じではありません。
例えば、
index.html:
<script src="a.js"></script>
<script src="b.js"></script>
a.js:
let value = "a";
b.js:
console.log(value);
とすると、b.jsからvalueを参照できます。
一方、
console.log(window.value);
では取得できません。
つまり、let、constはwindowとの結びつきを解消しましたが、通常Script間の名前共有まで解消したわけではありません。
let、constで解決されなかった問題
ここまで見ると、let、constによって多くの問題が解決されたことが分かります。
しかし、複数のJavaScriptファイルを組み合わせたときの問題は完全には解決されていません。
例えば、
a.js:
let value = "a";
b.js:
let value = "b";
を、
index.html:
<script src="a.js"></script>
<script src="b.js"></script>
として同時に読み込むと、
SyntaxError:
Identifier 'value' has already been declared
となります。
つまり、let、constはファイル単位の名前空間を提供する仕組みではありません。
また、通常の<script>では、
<script src="user.js"></script>
<script src="book.js"></script>
<script src="app.js"></script>
のようにHTML上の読み込み順によってJavaScriptファイル間の依存関係を管理する必要があります。
この問題を解決するために、ES Modulesがあります。
ES Modulesによるファイル単位の分離
Moduleとして読み込むと、それぞれのJavaScriptファイルは独立したModuleスコープを持ちます。
index.html:
<script type="module" src="a.js"></script>
<script type="module" src="b.js"></script>
a.js:
const value = "a";
console.log(value);
b.js:
const value = "b";
console.log(value);
この場合、それぞれのvalueは別のModuleスコープに存在します。
a.js
└── Module Scope
└── value = "a"
b.js
└── Module Scope
└── value = "b"
そのため、名前が衝突しません。
ES Modulesでは依存関係も明示できる
ES Modulesでは、import / exportによってファイル間の依存関係を明示できます。
user.js:
export function getUser() {
return "Alice";
}
app.js:
import { getUser } from "./user.js";
console.log(getUser());
app.jsを見るだけで、user.jsに依存していることが分かります。
通常の<script>では、
<script src="user.js"></script>
<script src="app.js"></script>
のようにHTML上の読み込み順に依存していました。
ES Modulesでは、JavaScript自身に依存関係を記述できます。
まとめ
varには、主に次のような問題がありました。
varの問題 |
let、constによる解決 |
|---|---|
| ブロックスコープを持たない | ブロックスコープを提供 |
| 同じスコープで再宣言できる | 再宣言を禁止 |
| 宣言前にアクセスできる | TDZによってエラーにする |
| 再代入しない意図を表現できない |
constによって表現できる |
トップレベルのvarがwindowと結びつく |
let、constはwindowのプロパティにならない |
一方で、次の問題はlet、constだけでは解決されません。
- JavaScriptファイル間の名前衝突
- JavaScriptファイル間の暗黙的な依存関係
- ファイル単位の名前空間
これらを解決するのがES Modulesです。
したがって、
var
↓
変数・スコープに関する問題
↓
let / const
↓
変数をより安全に扱えるようになった
↓
しかしファイル間の問題は残る
↓
ES Modules
↓
ファイル単位のスコープと依存関係を提供
という流れで理解すると、それぞれの役割を整理できます。
let、constは、varの変数宣言に関する問題を解決するための仕組みです。
一方、複数ファイル間の名前空間や依存関係を解決する役割を担うのがES Modulesです。
この2つを分けて考えることが、JavaScriptのスコープを理解する上で重要です。