var の落とし穴:なぜモダンJavaScriptで避けるべきか
はじめに
みなさん、こんにちは。私は業界歴 1 年半、JavaScript 歴も同じく 1 年半の hiro です。
正直なところ、最初はvar
について「なんとなく使わない方がいいらしい」という漠然とした認識しかありませんでした。ES6 以降のモダンな JavaScript しか触れてこなかった私にとって、var
はどこか遠い存在で、その問題点を深く理解する機会がありませんでした。
しかし、先輩エンジニアの方々とコードレビューを重ねる中で、var
の使用に対して厳しい指摘を何度か目にしました。「なぜvar
はそれほど問題なのか?」「let
やconst
とどう違うの?」そんな疑問が徐々に膨らんでいきました。
この記事は、そんな疑問を解消するために、自分自身の学びをまとめたものです。var
が非推奨とされる理由を、特にスコープの観点から掘り下げていきます。また、let
やconst
がどのようにこれらの問題を解決するのかについても触れていきます。
私と同じように、モダン JavaScript しか知らない方々にとって、この記事がvar
の問題点を理解し、より良いコーディング習慣を身につけるきっかけになれば幸いです。
コードの実行環境
HTML ファイルで js ファイルを読み込むというシンプルな構成でコードを実行しています。
Node.js で動かすのも考えましたが、インストールしていない方もいらっしゃるという想定なので Node.js は使用しない方針で進めます。
まずは結論から。なぜ非推奨なのか。
最後まで読む時間がない方はこの章だけでも覚えておいて下さい。
- グローバルスコープや関数スコープを作成し、ブロックスコープを無視する
- 変数の巻き上げ(ホイスティング)によって予期せぬ動作を引き起こす
- 同名の変数を再宣言できてしまう
この 3 点がlet
,const
との明確な違いであり、非推奨とされる所以だと私は確信しています。
それぞれ実際のコードを基に解説していきます。
解説の前に、そもそもスコープとは何か
早速「関数スコープ」「グローバルスコープ」「ブロックスコープ」というワードが出てきましたね。
今回はスコープの観点からvar
が非推奨とされる理由を解説するのでまずはスコープとは何か、押さえておきましょう。
MDN には以下のように記載されています。
実行の現在のコンテキスト。値 と式が「見える」、または参照できる文脈。変数や他の式が "現在のスコープ内にない" 場合、使用できません。スコープを階層構造で階層化して、子スコープから親スコープにアクセスできるようにすることもできますが、その逆はできません。
要は、スコープとは 「実行中のコードから値と式が参照できる範囲」 です。
もう少しかみ砕いた表現をすると、スコープは 「どこで変数や関数にアクセスできるかを決定するルール」 です。スコープの範囲によって、プログラムの異なる部分で同じ名前の変数を安全に使うことができます。
グローバルスコープについて
let a = 0;
const b = 0;
var c = 0;
function d() {}
debugger; // 実行を一時停止するもの
let
,const
,var
による変数宣言と、処理を行わない関数宣言を記述したサンプルコードです。
debugger
はブラウザの検証ツールを開いている状態で、その行に達した時点でコードの実行を一時停止させるために使用します。
検証ツールの「Scope」というタブにはdebugger
によって処理が止まっている行から見えるスコープが表示されています。
これはdebugger
が実行された行(つまり処理が一時停止している行)から見える変数がスクリプトスコープとグローバルスコープにそれぞれ存在していて、
-
let
,const
で定義した変数はスクリプトスコープのプロパティになる。 -
var
で定義した変数、関数宣言はグローバルスコープに属し、windowオブジェクト
のプロパティになる。
ということを表しています。
そのため、var
で定義した変数c
,関数d
はwindow.c
やwindow.d
として参照することが可能になります。
表にすると以下のようになります。
Script | Global(Window オブジェクト) |
---|---|
let | var |
const | function |
これがいわゆる「グローバルスコープ」です。
一般的には使い勝手が変わらないため「スクリプトスコープ」も「グローバルスコープ」として扱われますが、上記で示したような明確な違いがあるので注意が必要です。
関数スコープについて
関数スコープとは、その名の通り関数宣言に囲まれた{}
の中のスコープのことです。
function a() {
// この中で参照できるスコープ
}
ブロックスコープについて
ブロックスコープとは、{}
で囲まれた範囲内のスコープを指します。関数スコープとの違いは、関数宣言が不要な点です。基本的には、ブロック内で定義された変数はそのブロック内でのみ有効で、外からはアクセスできません。
ただし、ブロックスコープを有効にするには条件があります。それは、変数の宣言にlet
またはconst
を使用することです。var
で宣言された変数や関数宣言は、ブロックスコープを無視する特殊な動作をします。
実際にコードを書いて確認してみます。
{
let a = 1;
const b = 2;
var c = 3;
debugger;
}
コード実行後、 「ブロック」 という項目にlet
やconst
で宣言されたa
とb
が追加されました。一方、var
で宣言されたc
は、ブロックスコープではなくグローバルスコープに追加されます。これは、var
がブロックスコープを生成しないためです。
{
var c = 3;
function d() {
console.log("hello, world");
}
}
console.log(c); // 3
d(); // hello, world
var
で宣言された変数c
や関数宣言で定義した関数d
は、ブロックの外部からもアクセス可能です。これは、var
や関数宣言がブロックスコープを無視する挙動を持つためです。
このように、ブロックスコープを使用するためには、必ずlet
かconst
を使用する必要があり、var
での変数の定義、関数宣言はブロックスコープを無視するという特徴があります。
グローバルスコープや関数スコープを作成し、ブロックスコープを無視する
お待たせしました。
それでは実際のコードを使用してvar
による変数の定義が非推奨になる理由を見ていきます。
まずは 「グローバルスコープや関数スコープを作成し、ブロックスコープを無視する」 という理由です。
先ほども解説しましたがブロックスコープを作成するには、 let
もしくはconst
で変数を定義する という条件があります。
早速「ブロックスコープを無視する」という挙動を確認しましょう。
{
var x = 3;
console.log(x); // 3
}
x = 100;
console.log(x); // 100
このコードでは、ブロック内で定義した x
がブロックの外からも参照でき、さらに値を変更することができてしまいます。
これは変数のスコープが不明確になり、意図しない変数の使用や上書きを引き起こす可能性があります。
次に、let
(const
)を使用した場合の挙動を見てみましょう
{
let x = 5;
console.log(x); // 5
}
console.log(x); // ReferenceError: x is not defined
let
で定義された変数x
は、そのブロック内でのみ有効です。ブロックの外からx
にアクセスしようとするとReferenceError
が発生し、変数のスコープが明確に制限されていることがわかります。
このように、let
を使用することで変数のスコープがより明確になり、意図しない変数の使用や上書きを効果的に防ぐことができます。
変数の巻き上げ(ホイスティング)によって予期せぬ動作を引き起こす
ホイスティングとは、変数や関数の宣言が、そのスコープの先頭に移動されたかのように振る舞う JavaScript の特性です。
この特性は、コードの意図とは異なる動作を引き起こすことがあるため、特に初心者にとって注意が必要です。
次の例を見てみましょう。
console.log(x); // undefined
var x = 5;
console.log(x); // 5
このコードでは、var
で宣言された変数 x
が、実際の宣言よりも前に参照されていますが、undefined
が返ってきます。
これは、JavaScript が内部的に var x;
をスコープの先頭に移動させているからです。この「巻き上げ」によって、変数 x
が初期化される前に参照されてしまい、undefined
となるのです。
一方、let
や const
で宣言された変数は、このような巻き上げの影響を受けません。次のコードを見てみましょう。
console.log(x); // ReferenceError: x is not defined
let x = 5;
console.log(x); // エラーが発生しているため実行されない
このコードでは、let
で宣言された変数 x
を宣言前に参照しようとすると、ReferenceError: x is not defined
というエラーが発生します。
これは、let
や const
がブロックスコープを持ち、巻き上げが発生しないためです。この挙動によって、コードがより予測可能で安全になります。
以上がホイスティングによって予期せぬ動作を引き起こす例です。
理解が早い方は、下記コードのundefined
が返ってくる部分とTypeError
が返ってくる部分の違いについて疑問に思うかもしれません。
console.log(x); // undefined
var x = 5;
b(); // TypeError: b is not a function
var b = function () {
console.log("b is called!");
};
その辺についても軽くまとめてみたので時間がある方は以下のアコーディオンを開いてみて下さい。
変数参照と関数呼び出しにおける undefined と TypeError の違い
-
変数参照
console.log(x); // undefined var x = 5;
この場合、
var x
の宣言は巻き上げられますが、初期化(= 5
)は元の位置に残ります。そのため、console.log(x)
の時点ではx
は宣言されていますが、値は割り当てられていないのでundefined
になります。 -
関数呼び出し
b(); // TypeError: b is not a function var b = function () { console.log("b is called!"); };
この場合、変数
b
の宣言は巻き上げられますが、関数の割り当ては巻き上げられません。そのため、b()
を呼び出した時点では、b
はundefined
であり、関数ではありません。
undefined
な値を関数として呼び出そうとしているため、b is not a function
というエラーが発生します。
もし単にconsole.log(b)
とした場合はundefined
が表示されます。この挙動は関数宣言と関数式の違いを理解することが重要です。
- 関数宣言(
function b() {...}
)は全体が巻き上げられます。 - 関数式(
var b = function() {...}
)では変数宣言のみが巻き上げられ、関数の割り当ては元の位置に残ります。
- 関数宣言(
同名の変数を再宣言できてしまう
var
の最も問題となる特徴の 1 つは、同じスコープ内で同名の変数を何度でも再宣言できてしまうことです。
これは、コードの可読性を損ない、予期せぬバグを引き起こす可能性があります。
実際にコードで、この問題を具体的に見てみましょう。
var x = 10;
console.log(x); // 出力: 10
var x = 20; // 同名の変数を再宣言
console.log(x); // 出力: 20
// 同じスコープ内で何度でも再宣言が可能
var x = 30;
console.log(x); // 出力: 30
このコードでは、同じスコープ内で x
という変数が 3 回宣言されています。各宣言で x
の値が上書きされ、最後に宣言された値が使用されます。
これの何が問題かというと、
- 意図せずに既存の変数を上書きしてしまう
- どの宣言が実際に使用されているのかを追跡するのが困難になる
- 変数の生存範囲が予想以上に広くなり、意図しない箇所で変数が使用される可能性がある
- リファクタリングの際に変更の影響範囲を正確に把握するのが難しくなる
etc...
のようにあげればキリがないほど問題があります。
一方、let
や const
を使用すると、このような再宣言は許可されません。
let y = 10;
console.log(y); // 出力: 10
let y = 20; // SyntaxError: Identifier 'y' has already been declared
const z = 30;
console.log(z); // 出力: 30
const z = 40; // SyntaxError: Identifier 'z' has already been declared
let
や const
を使用すると、同名の変数を再宣言しようとした時点でSyntaxError
が発生します。これにより、変数の重複宣言を防ぎ、コードの意図をより明確にできます。
まとめ
この記事を通じて、var
の問題点と、なぜモダン JavaScript で避けるべきかを見てきました。主な理由は以下の 3 点です
- グローバルスコープや関数スコープを作成し、ブロックスコープを無視する
- 変数の巻き上げ(ホイスティング)によって予期せぬ動作を引き起こす
- 同名の変数を再宣言できてしまう
これらの問題は、コードの予測可能性を低下させ、バグの原因となる可能性があります。一方、let
とconst
を使用することで、これらの問題を回避し、より安全で読みやすいコードを書くことができます。
モダン JavaScript では、var
の使用を避け、代わりに
- 再代入が必要な変数には
let
- 定数や再代入が不要な変数には
const
を使用することを強く推奨します。これにより、変数のスコープがより明確になり、予期せぬ動作やバグを防ぐことができます。
私自身、この記事を書くことでvar
の問題点をより深く理解することができました。皆さんも、日々のコーディングでlet
とconst
を適切に使い分け、より安全で保守性の高い JavaScript コードを書いていってください。
Discussion