【実務・中級編】letとconstの使い分け:コードの意図を明確にするための命名と宣言のベストプラクティス – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

変数はなぜ `const` で始めるべきか? V8のメモリモデルから紐解くモダンJSのライフサイクル設計

コードレビューをしていて、未だに「とりあえず `let` で宣言しておき、後から再代入するかもしれないから…」という理由不明のコードに出会うことがある。

エンジニアよ、その設計は今すぐ捨ててほしい。

フロントエンドのコンポーネント設計、非同期API連携、そして膨大なDOMを扱うモダンWebアプリケーションにおいて、変数の宣言(`let` と `const`)の選択は、単なるコーディング規約の好みの問題ではない。それは「その変数がアプリケーションのライフサイクルの中でどう振る舞うべきか」という設計の意図をコンパイラとチームメイトに伝えるための最も重要な契約なのだ。

今回は、V8エンジンのメモリモデルとブラウザのレンダリングパイプラインを裏側でどう支配しているかという視点を交えながら、バグの起きない堅牢な変数のライフサイクル設計を叩き込む。

—

1. なぜ `const` がデフォルトなのか? V8の最適化と認知負荷の低減

「再代入しないから `const`」という説明は半分正しくて半分足りない。本質は「イミュータビリティ(不変性)の保証がもたらすランタイムの最適化と、人間の認知負荷の劇的な軽減」にある。

認知負荷の削減(アーキテクチャの観点)

現代のフロントエンド開発において、最大の敵はコードの複雑性である。100行を超える非同期処理関数の中で、`let` で宣言された変数がどこかで書き換えられている可能性を常に頭の片隅に置いておくのは、脳のRAMの無駄遣いだ。

`const` を使った瞬間、開発者はその変数について「このスコープ内において、この値(または参照)は絶対に変化しない」という絶対的な前提を持つことができる。これにより、コードのトレーサビリティ(追跡可能性)が跳ね上がりにくくなり、バグの温床を根絶できる。

V8エンジンにおける最適化の恩恵

V8エンジン(JavaScriptエンジン)は、変数が再代入されないことを静的に検知すると、内部のJITコンパイラ(TurboFanなど)による最適化の精度を上げる。再代入されない値は、定数としてインライン展開されたり、余計なポインタ追跡のコストを削減できたりするケースがある。

ただし、ここで一つ誤解を解いておこう。`const` は「値の不変(Immutability)」ではなく「バインディングの不変(Immutable Binding)」を意味する。オブジェクトや配列のプロパティが書き換え可能であることは、V8のヒープメモリ上でのアドレス参照が変わらないというメカニズムを理解していれば当然の挙動だ。

—

2. 実務で即効性のある設計パターン:DOM操作・配列処理・非同期API

では、実際のプロダクションコードでどのように `let` と `const` を使い分けるべきか。フロントエンド開発で頻出する3つのシーンを例に、ベストプラクティスを見ていこう。

シールされたコンテキスト:非同期API連携とDOM構築

以下のコードは、APIからデータを取得し、それを加工してDOMを構築するよくある処理だ。一見して「どこが変更され、どこが不変か」が美しく表現されている例を見てほしい。

/

  • ユーザープロファイルと設定を同時に取得し、ダッシュボードのDOMを構築する
  • @param {string} userId

/
async function renderUserDashboard(userId) {
// 1. 変更されることのない設定値やエンドポイントは const
const API_BASE_URL = ‘https://api.example.com/v1’;

try {
// 2. 非同期のフェッチ結果。再代入の必要がないため const で受ける
// (オブジェクトの中身は書き換えない、あるいは Object.freeze で堅牢性を高める)
const response = await fetch(`${API_BASE_URL}/users/${userId}`);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const userData = await response.json();

// 3. 配列やオブジェクトの加工には destructive なメソッド(push等)を避け、
// immutable なメソッド(map, filterなど)を使い、結果を const で受ける
const formattedPermissions = userData.permissions
.filter(permission => permission.active)
.map(permission => `role–${permission.name.toLowerCase()}`);

// 4. DOM要素への参照。要素自体の差し替え(再代入)は発生しないため const
// (※DOMのプロパティや子要素の変更は、参照自体の変更とは別)
const container = document.getElementById(‘dashboard-root’);
if (!container) {
throw new Error(‘Dashboard root element not found in DOM tree.’);
}

// フラグメントを用いてレンダリングパイプラインへの負荷(Reflow/Repaint)を最小化
const fragment = document.createDocumentFragment();

const titleElement = document.createElement(‘h1’);
titleElement.textContent = `Welcome, ${userData.name}`;
fragment.appendChild(titleElement);

formattedPermissions.forEach(roleClass => {
const badge = document.createElement(‘span’);
badge.className = `badge ${roleClass}`;
badge.textContent = roleClass;
fragment.appendChild(badge);
});

// 一括でDOMツリーに挿入(ブラウザのレイアウト計算コストを1回に抑制)
container.replaceChildren(fragment);

} catch (error) {
// エラーハンドリングにおけるロギング。console も再代入不可
console.error(‘[Dashboard Render Error]:’, error);
// UIへのフォールバック表示など
}
}

テクニカルリードからの指摘:なぜこのコードが優れているのか?

  • 不要な `let` の排除: `response`, `userData`, `container` すべてが `const` で宣言されている。これにより、「この変数が後から書き換えられるかもしれない」というノイズがコードから完全に排除されている。
  • DOMパフォーマンスへの配慮: `container` 自体は `const` で固定しつつ、`DocumentFragment` を活用してブラウザのReflow(再レイアウト)とRepaint(再描画)の回数を極限まで絞り込んでいる。

—

3. では、いつ `let` を使えばいいのか?(唯一の正当なユースケース)

`let` の存在価値は、「スコープ内において、状態が明確に変化する(再代入される)ことが仕様として決まっている変数」に限定されるべきである。

典型的な例は、ループカウンタや、逐次的に集計・変化していくアキュムレータ(累積変数)だ。

/