JavaScript/TypeScript

let と const とスコープ

4
  • JavaScript

let と const とスコープ — JavaScript Tips

変数宣言は letconst が標準です。古い var はブロックスコープを持たず、巻き上げ(hoisting)の挙動も独特なため、新しいコードでは避けます。const は「再代入できない」だけで、「中身が不変」という意味ではありません。

参考: MDN — let · MDN — const · MDN — スコープ


目次

  1. let / const / var の違い
  2. ブロックスコープ
  3. TDZ(一時的デッドゾーン)
  4. const と let の使い分け
  5. var を避ける理由
  6. うまくいかないとき

let / const / var の違い

宣言 スコープ 再代入 再宣言(同じ名前)
let ブロック 不可(同スコープ内)
const ブロック 不可 不可
var 関数(レガシー)
let count = 0;count = 1; // OKconst MAX = 100;// MAX = 101; // TypeError: Assignment to constant variable.var legacy = 1; // 新規コードでは使わない

基本方針: デフォルトは const、再代入が必要なときだけ let を使う。


ブロックスコープ

{ } で囲んだブロック内で宣言した変数は、そのブロックの外から見えません。

{  const secret = "ブロック内";  console.log(secret); // OK}// console.log(secret); // ReferenceErrorif (true) {  let temp = 42;  console.log(temp);}// console.log(temp); // ReferenceError

for ループもブロックです。

for (let i = 0; i < 3; i++) {  console.log(i);}// console.log(i); // ReferenceError(let のとき)
flowchart TB  subgraph outer["関数スコープ"]    A["const a = 1"]    subgraph block["if ブロック"]      B["let b = 2"]    end  end  block -.->|外からは見えない| X["b は参照不可"]

var との対比(なぜ問題か)

if (true) {  var leaked = "漏れる";}console.log(leaked); // "漏れる"(var はブロックを貫通)

意図せず外側から見える変数は、名前衝突やデバッグ困難の原因になります。


TDZ(一時的デッドゾーン)

letconst は、宣言行より で参照すると ReferenceError になります。この区間を TDZ と呼びます。

// console.log(message); // ReferenceError(TDZ)let message = "こんにちは";console.log(message); // OK

var は巻き上げにより宣言前でも undefined として参照できてしまい、バグの温床です。

console.log(old); // undefined(エラーにはならない)var old = 1;

for ループの定番パターン

const buttons = ["A", "B", "C"];for (let i = 0; i < buttons.length; i++) {  const label = buttons[i];  // 各反復で新しい i, label ができる  console.log(i, label);}

var i だとループ後の i が共有され、非同期コールバックと組み合わせたときに壊れやすいです。


const と let の使い分け

const は再代入不可

const PI = 3.14159;const user = { name: "花子" };user.name = "太郎"; // OK(オブジェクトの中身は変更可)// user = {};       // エラー(別オブジェクトへの再代入は不可)
ケース 推奨
数値・文字列など再代入しない const
ループカウンタ、累積値 let
オブジェクト・配列の「箱」 const(中身の変更は可)
後から代入し直す参照 let
const items = [];items.push("a"); // OKlet total = 0;total += 10;     // let が必要

「不変にしたい」のは 参照 なのか 中身 なのかを分けて考えます。深い不変は Object.freeze や設計で扱い、日常は const + 意図した変更で十分なことが多いです。

宣言はなるべく使う直前に

function process(data) {  const normalized = data.trim().toLowerCase();  let result = "";  for (const ch of normalized) {    result += ch;  }  return result;}

スコープを狭くすると、どこで何が使われているか追いやすくなります。


var を避ける理由

  1. ブロックスコープがないiffor の中の var が外に漏れる
  2. 巻き上げで TDZ がない — 宣言前に undefined で触れてしまう
  3. 同スコープで再宣言できる — typo で上書きに気づきにくい
function broken() {  if (true) {    var x = 1;  }  if (true) {    var x = 2; // 同じ x を再宣言・上書き  }  console.log(x); // 2}

レガシーコードを読むときだけ var に出会います。新規コードは let / const にします。

モダンな置き換えの例

// 避けたいfor (var j = 0; j < 3; j++) {  setTimeout(() => console.log(j), 100);}// 3, 3, 3 と表示されがち// 推奨for (let j = 0; j < 3; j++) {  setTimeout(() => console.log(j), 100);}// 0, 1, 2

うまくいかないとき

症状 原因 対処
ReferenceError: Cannot access 'x' before initialization TDZ 内で let/const を参照 宣言を上に移す。宣言前に使わない
Assignment to constant variable const へ再代入 let にするか、新しい変数名を使う
ループのコールバックがおかしい var でインデックス共有 let または for...of
ブロック外で変数が見えない ブロックスコープが効いている 必要ならブロック外で宣言(ただしスコープは広げすぎない)
const なのに配列を変更できる const は参照の固定のみ 意図どおりなら問題なし。完全凍結は別手段

関連: MDN — var · MDN — クロージャ

シェア