JavaScriptでvarが非推奨な理由を整理してみた


はじめに

未経験からNode.jsの現場に配属された2019年新卒エンジニアが、学習の振り返りとしてJavaScriptの基礎の基礎をまとめます。

今回は、JavaScriptの変数宣言についてあらためて整理し、なぜ近年のJavaScriptではvarの使用が推奨されていないのかをまとめたいと思います。

こういう人に読んでもらいたい

  • JavaScriptを勉強したてで、ひとまず変数にはvarを使ってる人
  • letやconstを使っているけど、改めて訊かれると理由を上手く説明できない人

JavaScriptにおける変数宣言をおさらい

本題に入る前に、まずJavaScriptの宣言に関する基本的な挙動を整理します。

宣言の種類は?

2019/12月現在、JavaScriptの変数宣言キーワードにはvar, let, constの3つがある。
元々はvarしかなかったところに、ECMAScript2015(ES6)から新たにlet, constが導入。

どうやって宣言する?

「宣言キーワード + 変数名」で宣言。
JavaScriptでは宣言時点での型の制約がなく、var, let, constの3種類だけであらゆる型を取り扱える。
文字列、数字、Booleanといった基本型のみならず、オブジェクトや関数を変数と同じように宣言することが可能。

var pokemon = "サナギラス";
var level = 55;
var isEvolve = true;

var type = { type1:"いわ", type2:"じめん" }; // object

// 関数も変数として宣言できる
var evolution = function(monster){
  if(monster === "サナギラス") {
    monster = "バンギラス";
  }
  return monster;
}

console.log(evolution(pokemon)); // ログの結果には「バンギラス」と表示される

上記のvarはlet, constでも置き換え可能。

ほかの特徴は?

宣言時に何も代入をしないとundefined(未定義)となる

変数を宣言した際に値を代入しない場合は、undefinedを代入した状態の変数として見なされる。

var pokemon; //undefined

pokemon = "サナギラス";  // varかletで宣言していれば、このように後から再代入できる

キーワードなしでも一応宣言できる  

var, let, constをつけなくても変数宣言は成立し、関数内で宣言してもグローバルに参照できる変数となる。

ただし、バグの温床となるのでやめた方が良いとされている。strictモードでは使用が禁じられている。

const global = () => {
   pokemon = "コイキング";
};

global();
console.log(pokemon);  // ログの出力結果は「コイキング」

「巻き上げ」と呼ばれる独特の挙動がある

JavaScriptの変数には「巻き上げ」という、他の言語にはおそらく存在しないであろう特徴があります。どんな特徴かというと、関数の内部で宣言されている変数は、その関数の先頭で宣言されたものと見なされるというものです。


var pokemon = "ニドラン♂";

var box = function() {
    console.log(pokemon); // ログの出力結果は「ニドラン♂」ではなく「undefined」
    var pokemon = "ニドラン♀";
    console.log(pokemon); // ログの出力結果は「ニドラン♀」
};

var box の関数内部で var pokemon = “ニドラン♀” と記述されたことで「巻き上げ」が起こり、box関数の先頭でpokemonという変数が宣言されたと見なされます。宣言だけされたので値はまだ入っていないと認識された結果、1つ目のログ出力部分ではundefinedと判定されています。

つまり、実際には以下のように関数の先頭で変数が改めて宣言されている挙動になっています。捉え方としては、先頭でundefined型の変数を宣言して、後ろの行で再代入している、というイメージです。


var pokemon = "ニドラン♂";

var box = function() {
    var pokemon;
    console.log(pokemon); //undefined
    pokemon = "ニドラン♀";
    console.log(pokemon); //ニドラン♀
};

まずはこれだけ見ておけばいいよという表を作ってみた

JavaScriptの変数に共通する特徴を整理したところで、3つのキーワードの違いを掘り下げます。
var, let, constの違いを端的に表にすると、再代入・再宣言・スコープで以下のような違いがあります。

var let const
再代入 可能 可能 不可能
再宣言 可能 不可能 不可能
スコープ 関数スコープ ブロックスコープ ブロックスコープ

キーワード別に、詳しく解説していきます。

let

再代入が可能だが再宣言を受け付けない変数。

再代入はできるが、再宣言はできない

以下はvarだと問題なく実行できるが、letだと再宣言のタイミングでエラーになる。

let pokemon = "フシギダネ";

pokemon = "フシギソウ";
console.log(pokemon); // 再代入されたので、出力結果が「フシギソウ」となる

let pokemon = "フシギバナ"; // 再宣言したタイミングでSyntaxErrorが発生する

スコープはブロックスコープ

スコープはブロックスコープのため、if文やtry-catch文などを用いた際の挙動がvarと異なる。

varの場合、同じ関数内部であれば同じスコープなので、入れ子になっていても再代入される。

/* varの場合 */
var evolution = () => {
  var pokemon = "フシギダネ";
  console.log(pokemon); 

  {
    var pokemon = "フシギソウ";
    console.log(pokemon); 
  }

  console.log(pokemon);
};

evolution();
  /*
    出力結果は以下の順番で表示される    
    フシギダネ
    フシギソウ
    フシギソウ
  */

letの場合はブロックスコープとなるため、同じ関数内部でもブロックを分けることで再代入の影響を受けなくなる。

/* letの場合 */
let evolution = () => {
  let pokemon = "フシギダネ";
  console.log(pokemon);

  {
    let pokemon = "フシギソウ";
    console.log(pokemon);
  }

  console.log(pokemon);
};

evolution();
  /*
    出力結果は以下の順番で表示される    
    フシギダネ
    フシギソウ
    フシギダネ
  */

const

再代入も再宣言もできない定数として扱える変数を宣言する場合に利用するキーワード。

再代入も再宣言もできない

const pokemon = "ヒトカゲ";

pokemon = "リザード"; // 再代入しようとするとTypeErrorが発生する

const pokemon = "リザードン"; // 再宣言しようとするとSyntaxErrorが発生する

上記は実行時に再代入しようとした箇所と再宣言しようとした箇所でエラーが表示されてしまう。

再代入はできないものの、objectの中身を変えたりはできるので、値の変更を全く受け付けないわけではない。以下のコードのようにobjectの中身を書き換えて実行することは可能。

const pokemon = {
  name: "リザード",
  type: "ほのお"
};

pokemon.name = "リザードン";
pokemon.type = "ほのお・ひこう";

console.log(pokemon); // 出力結果は「{ name: "リザードン", type: "ほのお・ひこう" }」

スコープはブロックスコープ

スコープはletと同じでブロックスコープです。ただし、再代入ができないので、うっかりtry-catch文で囲んでしまうとスコープから外れてしまうので注意です...。


try {
  const pokemon = { name: "カメックス" };
} catch (e) {
  console.log(e);
}

const name = pokemon.name; // ブロックが異なる箇所で宣言されているためReferenceErrorとなる

try-catchを利用する場合はletを駆使して以下のように書くこと。

let pokemon;
try {
  pokemon = { name: "カメックス" };
} catch (e) {
  console.log(e);
}

const name = pokemon.name; 
console.log(name); // 出力結果は「カメックス」

それで、結局どうしてvarは良くないのか

varが良くないとされる理由は、おおむね以下の2点に集約されると思います。

  1. 変数を簡単に書き換えられてしまうと、意図しないバグが発生するため
  2. letやconstに比べて、varは巻き上げ時のバグを生み出しやすいため

変数を書き換えるとバグの温床になる

長いコードを書いていたりすると、うっかり二重で宣言してしまったり、ブロック内で書き換えた値が関数全体に影響してしまったり、というリスクはつきもの。
そんなとき、letやconstを使っていれば、意図せず書き換えてしまう前にエラーを出してくれるので、バグに気づきやすくなるのです。

変数の書き換えという意味では再代入も本当は控えたいのですが、for文や先述したtry-catch文など、どうしても再代入が必要な場面はあります。そのようなときは、他に影響が出ないことを確認した上でletを使うのが良いでしょう。

巻き上げが発生すると意図しない挙動になる恐れがある

巻き上げについても考え方は同様で、letやconstでは代入前に変数を参照するとReferenceErrorとして扱われるため、うっかりundefinedが代入されたまま処理を続けてしまう状態を防ぐことができます。

const pokemon = "ニドラン♂";

const box = function() {
    console.log(pokemon); // ReferenceError  varだとundefined
    const pokemon = "ニドラン♀";
    console.log(pokemon); //ニドラン♀
};

厳密にはletやconstでも巻き上げが起こっているらしいのですが、挙動としては巻き上げが発生してもエラーになるので、letやconstなら巻き上げを防げるとインプットしてしまって問題ないかと思われます。

まとめ

  • varはブロックスコープにできない、再代入も再宣言も防げない、巻き上げも起きる、よって✖
  • 基本はconstで宣言する
  • 再代入が必要そうなときだけletを使う

以上です。
間違い、不足点の指摘などあれば、コメントをお願いいたします。