エンジニアの皆さん、コードレビューの場で「なぜまだ `var` を使っているのか?」と問いただす側になっていないだろうか。
現代のJavaScript開発において、`var` の使用は百害あって一利なしだ。ES2015(ES6)の黎明期から幾度となくその非道性が叫ばれてきたにもかかわらず、いまだにレガシーなコードベースや、表面的な理解のまま書かれたスクリプトの中には `var` の残骸が息を潜めている。
今回は、V8エンジンのメモリモデルと実行コンテキストの挙動にまで踏み込み、なぜ `var` が現代の開発において「悪」であり、直ちに撲滅されなければならないのかを、実務的なコードの実験を通してロジカルに解き明かしていく。
—
1. 実行コンテキストと変数ホイスティングの闇
まず、JavaScriptエンジン(V8など)がコードを解釈し、実行するメカニズムの根本から振り返ろう。JavaScriptはインタプリタ言語のように見えて、実際には実行前に「Creation Phase(生成フェーズ)」と「Execution Phase(実行フェーズ)」という2つのステップを経る。
この生成フェーズにおいて、`var` で宣言された変数は、そのスコープの最上部に「巻き上げ(Hoisting)」され、自動的に `undefined` で初期化される。
console.log(userId); // エラーにならず、”undefined” が出力される
var userId = ‘EMP-8842’;
console.log(userId); // “EMP-8842”
これの何が問題か。言語仕様上、「宣言される前に変数がアクセス可能(ただし値は `undefined`)」という、人間心理に反する挙動を許容してしまうことだ。大規模な非同期処理や複雑なコンポーネントのライフサイクルにおいて、この挙動は「存在しないはずのデータが参照できてしまう」という致命的な論理バグ(Silent Failure)の温床となる。
TDZ(Temporal Dead Zone)という福音
一方、`let` や `const` も巻き上げは起きる。しかし、これらは生成フェーズで初期化を行わない。宣言の行に到達するまでの間、その変数は「時間的デッドゾーン(TDZ)」に置かれ、アクセスしようものなら即座に `ReferenceError` がスローされる。
console.log(apiKey); // ReferenceError: Cannot access ‘apiKey’ before initialization
let apiKey = ‘secret_fx9902’;
バグは「隠蔽される」よりも「即座にクラッシュして教えтеくれる」方がはるかに安全である。`let` と `const` は、言語レベルでバグを早期発見させるための防壁なのだ。
—
2. スコープ汚染の実験:なぜ `var` は関数を突き抜けるのか
`var` の最大にして最悪の特性は、それが「関数スコープ」しか持たない点にある。`if` 文や `for` ループといった、いわゆるブロック( `{}` )を完全に無視して外側に漏れ出す。
以下の実務さながらのデータ処理パイプラインのコードを見てほしい。
/
- レガシーな在庫データ集計処理(反面教師コード)
- @param {Array
/
function processInventory(items) {
var totalValue = 0;
for (var i = 0; i < items.length; i++) { var item = items[i]; var itemSubtotal = item.price item.quantity; // 条件分岐の中でうっかり再宣言・上書きしてしまうケース if (item.isDiscounted) { var itemSubtotal = itemSubtotal 0.8; // varはブロックを無視するため、外側の itemSubtotal が上書きされる! } totalValue += itemSubtotal; } // 恐ろしいことに、ループを抜けた後もこれらの変数は関数スコープ内を通じて生存し続ける console.log('最後に処理されたアイテム名:', item.name); // 漏れ出している console.log('ループカウンターiの値:', i); // i = items.length が外から参照できてしまう return totalValue; } このコードの何が危険か。`for` ループのカウンター変数 `i` や、ブロック内で計算したはずの `itemSubtotal` が、関数スコープ全体にリークしている。 もしこれが非同期処理(`setTimeout` や `Promise`)と絡み合った場合、クロージャを通じて意図しない変数の書き換えを引き起こし、デバッグが極めて困難な競合状態(Race Condition)を生み出す。
プロダクションコード:`let` と `const` による完全カプセル化
これをモダンなJavaScript(ES2015以降)の作法に則り、ブロックスコープを厳格に適用した堅牢なコードにリファクタリングする。
/
- モダンな在庫データ集計処理(プロダクション品質)
- @param {Array
>} items - @returns {number}
/
function processInventorySecure(items) {
let totalValue = 0;
for (let i = 0; i < items.length; i++) { const item = items[i]; let itemSubtotal = item.price item.quantity; if (item.isDiscounted) { // letを使うことで、このブロック内だけのスコープとして安全に再定義・計算が可能 // 外側の itemSubtotal とは完全に分離される itemSubtotal = 0.8; } totalValue += itemSubtotal; } // 🔴 完全にスコープが閉じられているため、ここで i や item を参照しようとすると ReferenceError になる // console.log(i); -> デザイナやレビュアーの意図せぬグローバル/関数汚染をコンパイル前・実行時エラーで阻止
return totalValue;
}
さらに言えば、配列の集計処理であれば、そもそも `for` ループすら排除し、`Array.prototype.reduce` や `map` といったイミュータブルな配列メソッドを `const` と共に用いるのが、モダンフロントエンドのベストプラクティスだ。
const calculateTotalValue = (items) => {
return items.reduce((accumulator, item) => {
const subtotal = item.price item.quantity;
const actualPrice = item.isDiscounted ? subtotal 0.8 : subtotal;
return accumulator + actualPrice;
}, 0);
};
これにより、変数の再代入(Mutation)の余地を完全に排除し、V8エンジンのJITコンパイラにとっても最適化しやすい(インライン展開や隠しクラスの維持が容易な)コード構造が完成する。
—
3. グローバルオブジェクトの汚染:ブラウザ環境における致命傷
もう一つの重大な問題は、グローバルスコープ(どの関数にも属さない最上位のコンテキスト)で `var` を使った場合に引き起こされる。
ブラウザ環境において、グローバルスコープで `var` を宣言すると、それはグローバルオブジェクト(ブラウザなら `window`)のプロパティとしてアタッチされる。
var currentUser = ‘Admin_A’;
console.log(window.currentUser); // “Admin_A” (勝手にwindowに生える)
// さらに恐ろしいことに…
window.currentUser = ‘Hacker_Modified’;
console.log(currentUser); // “Hacker_Modified” (予期せぬ外部からの上書きリスク)
サードパーティ製のスクリプトやCDN経由のライブラリが乱立する現代のフロントエンドにおいて、グローバルオブジェクトのプロパティを勝手に汚染・共有する行為は、セキュリティ脆弱性(グローバル変数ポイズニング)の温床となる。
一方、トップレベルでの `let` や `const` は、グローバルオブジェクトのプロパティにはならない(Scriptスコープという独立した領域に配置される)。
let appConfig = { env: ‘production’ };
console.log(window.appConfig); // undefined (windowを汚染しない!)
この仕様の違いだけでも、保守性と安全性が劇的に向上することが分かるはずだ。
—
テクニカルリードからの提言:今日からチームで徹底すべきこと
コードレビューの現場で `var` を見かけたら、それは単なる「古い書き方」として見過ごしてはならない。それは「メモリ管理やスコープの概念を理解していないシグナル」であり、将来のバグの芽そのものだ。
1. ESLintの導入: ESLintを使用しているなら、`no-var` ルールおよび `prefer-const` ルールを必ず `error` に設定し、CI/CDパイプラインで自動的に弾く仕組みを構築せよ。
2. デフォルトは `const`: 変数を宣言する際は、まず `const` を使え。再代入がどうしても必要な(カウンターやループなど最小限の)変数にのみ `let` を許可し、`var` は語彙の中から完全に抹消せよ。
型安全性、スコープの封じ込め、そして予測可能なメモリ管理。これらを制する者が、モダンWebアプリケーションのパフォーマンスとスケーラビリティを制する。妥協のないコードベースを築き上げよう。