3つのキーワード、役割は1つ
JavaScriptで変数を宣言する方法は3種類あります。var、let、constです。どれも名前に値を結びつけるという点では同じですが、スコープ、再代入のルール、そして宣言前の挙動がそれぞれ違います。最近のコードでは const をメインに使い、値を変える必要があるときだけ let、var はほぼ使わない、というのが定番のスタイルです。
ざっくりまとめると次のとおりです。
このページの残りでは、さっきの3行がなぜそう振る舞うのかを掘り下げていきます。
const:まずはこれを使う
const で宣言した変数は、再代入ができません。一度 const x = 5 と書いてしまえば、あとから x = 6 とはできず、エンジンが TypeError を投げます。
これがルールのすべてです。きちんと書かれたプログラムでは、ほとんどの変数は再代入する必要がないので、constで大半のケースは事足ります。まずconstから手を伸ばすようにしておけば、値が実際に 変化する 場所が自然と際立ちます。
ここでよく誤解されるのが、constが守ってくれるのは 束縛(バインディング) であって 値そのもの ではない、という点です。値がオブジェクトや配列の場合、中身は依然として書き換え可能です。
const が保証するのは「この名前は常にこのオブジェクトを指し続ける」ということであって、「このオブジェクトは変わらない」ではありません。オブジェクト自体を変更不可にしたいなら Object.freeze(user) の出番です。ただ実際のコードでは、「const で宣言したオブジェクトはミューテートしない」という暗黙のルールに頼っているケースがほとんどです。
let は再代入が本当に必要なときに使う
let は再代入できるという点を除けば、const とまったく同じです。カウンタ、アキュムレータ、ループ変数など、値が時間とともに実際に変化する場面で使いましょう。
let で宣言したのに一度も再代入していない、と気づいたら const に切り替えましょう。最近のプロジェクトならたいていリンターが指摘してくれます。
ブロックスコープ
let と const はどちらも ブロックスコープ を持ちます。ブロックというのは { と } で囲まれた範囲のことで、if 文の中身、for ループの中身、関数の本体、あるいは単独で書いた { ... } でも構いません。ブロックの中で宣言した変数は、そのブロックの外からは存在しないものとして扱われます。
これこそが欲しい挙動です。変数がそれぞれの仕事の範囲内に収まり、名前の衝突も起こしようがなくなります。
var はそうなっていません。これが var を避けるべき最大の理由です。
var: 関数スコープと思わぬ落とし穴
var は一番近い 関数 がスコープになります。ブロック単位ではありません。そのため、if や for の中で宣言した var は、外側の関数にまで漏れ出してしまいます。
このゆるいスコープこそ、JavaScriptの古典的なバグの温床になってきました。中でも有名なのが、ループ内で全イテレーションが同じ var i を共有してしまい、コールバックがどれもこれも最終値を参照してしまう、というあのパターンです。
さらに厄介なのが、var は同じスコープ内で同名の変数を何度でも再宣言できてしまうこと。おかげでタイポが静かに紛れ込みます:
最近のコードでは let と const がほぼ全てで、var を見かけるのはレガシーなコードベースや古い Stack Overflow の回答、それから相当古い環境で動かす必要があるスクリプトくらいです。
巻き上げ(ホイスティング)と Temporal Dead Zone
3 つの宣言はどれも 巻き上げ(hoisting) されます。つまり、ブロック内のコードが実行される前にエンジンが宣言の存在を把握しているということです。ただし、宣言の行が実行される前の挙動はそれぞれ違います。
var は巻き上げられたうえで undefined に初期化されます。そのため、var の行より前で参照してもエラーにはなりません。
let と const も巻き上げ自体はされますが、初期化はされません。宣言より前にアクセスするとエラーになります。スコープに入ってから宣言文が実行されるまでのこの区間を、temporal dead zone(TDZ、一時的死角) と呼びます。
TDZ はバグではなく、むしろ便利な仕様です。「宣言前に使った」ときに黙って undefined を返すのではなく、きちんとエラーとして知らせてくれるので、タイプミスや順序ミスをかなりの確率で拾えます。
for...of ループでの const
ちょっとしたテクニックですが覚えておくと便利なのが、for...of ループです。このループは反復ごとに新しいバインディングを作るため、値が「毎回変わる」ように見えてもループ変数に const を使えます。
ループの各反復ごとに name は新しい束縛(バインディング)として作られるので、同じ変数を使い回しているわけではありません。一方、昔ながらの for (let i = 0; i < n; i++) ではやはり let が必要です。というのも、i はひとつの束縛を書き換え続けているからです。
const let 使い分けの実践ルール
変数宣言は次の優先順位で選ぶのがおすすめです。
- まずは
const。再代入しない値なら、そう宣言で明示しましょう。 letは、その束縛を本当に書き換える必要があるときだけ使う。varは、どうしても使わざるを得ないコードベースに限る。
これを徹底すれば、「この変数は再代入されるのか」「スコープはこのブロックなのか関数全体なのか」が一目でわかるようになります。
MAX_RETRIES と users は一度決めたら変わらないので const。successful は途中で増えていくので let。user はループのたびに新しくバインドされるため const でOKです。コードを上から順に読んでいくだけで、動く値と動かない値がひと目で分かりますよね。実行しなくても意図が伝わるのがポイントです。
次は: プリミティブ型
変数の宣言ができるようになったら、次に気になるのは「その変数にどんな値を入れられるのか」です。JavaScriptには数値・文字列・真偽値をはじめ、いくつかのプリミティブ型が用意されていて、それぞれにちょっとしたクセがあります。詳しくは次のページで見ていきましょう。
よくある質問
JavaScriptのlet・const・varは何が違うの?
constとletはES2015で追加されたブロックスコープの宣言、varは昔からある関数スコープの宣言です。constは再代入不可、letは再代入できます。さらにvarは巻き上げられてundefinedで初期化されるのに対し、letとconstは巻き上げはされるものの宣言行に到達するまで参照できません(これが「一時的死角(TDZ)」と呼ばれる状態です)。
constで宣言したら値は変更できなくなる?
constで宣言したら値は変更できなくなる?いいえ。constが禁止しているのは「再代入」だけで、値そのものの中身は変えられます。たとえばオブジェクトや配列なら、const user = {}; user.name = 'Ada'のようにプロパティを書き換えるのは普通にOKです。本当に不変にしたいならObject.freezeやImmutable系のライブラリを使いましょう。
今でもvarを使う必要はある?
varを使う必要はある?ほとんどありません。letとconstの方がスコープが明確で、構文解析の段階でバグに気づけることも多いです。varに出会う場面といえば、古いコードベースの保守か、ES2015非対応の環境で動かす必要がある一部のスクリプトくらいです。