まだまだBabelにお世話になりつつも、ES2015ベースでコードを書くことが確実に増えて来ていますね。
ES2015では変数の宣言に let, const という新しいシンタックスが使えるようになりました。
早速これらを使うようにしてみたのですが、let/constについては「letはブロックスコープ」「constは定数」程度の認識だったので、私の書いたコードには var, let, constが入り混じるというよくわからない状態になりました。
そのような状況を経て、まず最初に思ったのが「これ、var 要らなくね?」というもの。
基本的に、変数の有効範囲(スコープ)は可能な限り局所的にすべきという考えだったので、それであれば var よりも let で変数を宣言した方が好ましいからです。
一方、const に関しては定数を宣言するためのもので、自分の中では円周率(Math.PI)的なものや、コンフィグレーションのようなものにしか使わない認識でいたのですが、最近のJS界隈のサンプルコードを見ていたりすると、そういった用途に限らず結構使われている印象がありました。ここでも私の認識とのギャップがあるような気がしたわけです。
で、いよいよワケが分からなくなってきたため、一度 var, let, const の「使い分けかた」について勉強がてら調査&まとめました。これは今後の自分自身のコーディングルールとして採用することを念頭に置いています。
とりあえず結論から
いきなり結論から書きますが、色々と調べてみるとどうも以下の方法がベストプラクティスらしいです。
- 原則 var は使わない
- 変数宣言は基本的に const で
- 変数への再代入が必要な場合に限り let を使う
let/const ではブロックスコープが有効になります。このことによりvarの必要性はほぼ無くなりました。varを使用しなくてもlet/constで通常は事足りますしその方が安全です。
varとlet/constとではスコープ以外にもいくつかの挙動の違い(後述)がありますが、通常、var特有の挙動が必要になるケースというのは無いと思います。どうしてもvarが必要になるケースがあった場合、それはあくまでも特殊なケースであると言えるでしょう。
私が感じていた「var不要説」については、基本的な認識としては良さそうでした。
いくつかの参考記事によると、基本的に変数は const で宣言し、再代入が必要な変数のみ let を使う。さらにどうしても var が必要なケースのみ var で宣言する、というのがベストプラクティスのようです。
確かに、上書されたくない変数をわざわざ var や let で宣言する必要はないでしょう。また、const で宣言された変数は再代入されることが無いので、変数の内容を追跡する必要が無いということはデバッグ時の負担を軽減してくれるというメリットもあります。
個人的な意見ですが、変数の再代入が必要になるケースというのはそれほど多くないと私は思います。例えば、条件によって変数の初期値が変わるような場合でも三項演算子が使えるかもしれませんし、ループ処理においても forEach, map といった配列メソッドを使えばインクリメント用の変数も使わずに済むでしょう。JavaScriptの場合はうまく関数を組み合わせることで変数自体を減らすこともできます。
次節以降では、var, let, const についてそれぞれの挙動の違いについてもう少し掘り下げてみます。
ES5時代のスコープ
まずはちょっとしたおさらいですが、ES5までは let/const は有りませんので、通常、変数宣言には var を使用します。var で宣言された変数は「関数スコープ」ですので、例えば ifブロックの中で var で宣言された変数は関数内であればどこからでも参照可能です。
function func() { if (true) { var hoge = 'hoge'; } console.log(hoge); // hoge } func();
変数の宣言にvarしか使えないということは、ブロックスコープの変数を作成することが出来ません。ES5以前では、即時関数を使うことで疑似的にスコープを作り出しこの問題に対処していました。
function func() { (function () { var hoge = 'hoge'; console.log(hoge); // hoge }()); console.log(hoge); // ReferenceError: hoge is not defined } func();
ES2015からlet, constが使えるようになった
ES2015からは従来のvarに加え、新たに変数宣言用のシンタックスとしてlet, constが使えるようになりました。varとlet/constでは以下に示すような違いがあります。
■ブロックスコープ
varで宣言された変数が「関数スコープ」であるのに対し、let/constで宣言された変数は「ブロックスコープ」になります。
(function () { if (true) { var hoge = 'hoge'; let fuga = 'fuga'; const piyo = 'piyo'; console.log(hoge); //hoge console.log(fuga); //fuga console.log(piyo); //piyo } console.log(hoge); //hoge console.log(fuga); //ReferenceError: fuga is not defined console.log(piyo); //ReferenceError: piyo is not defined }());
let/constが使える場合、ブロックスコープを作るためだけの即時関数は不要です。
{ let fuga = 'fuga'; const piyo = 'piyo'; } console.log(fuga); //ReferenceError: fuga is not defined console.log(piyo); //ReferenceError: piyo is not defined
余談ですが、let/constを使えば、switch文の各caseの中にスコープを作成することもできます。通常、case文はスコープを作りませんので、次の様に書いてしまうとエラーになります。(※エラーが発生する、ということを書きたかっただけなのでコードの妥当性はひとまず置いといてください。)
switch (name) { case 'taro': const text = 'my name is taro'; break; case 'jiro': const text = 'my name is jiro'; break; default: const text = 'my name is nanashi'; //SyntaxError: Identifier 'text' has already been declared break; }
上記のswitch文は下記の様に各case文をブロックで囲むことでエラーを回避することができます。
switch (name) { case 'taro': { const text = 'my name is taro'; break; } case 'jiro': { const text = 'my name is jiro'; break; } default: { const text = 'my name is nanashi'; break; } }
case の中でスコープを作成するようなケースはあまりないと思いますが、Reduxとかを使っていると、caseの中で処理を書き込むことが多いので、いざという時のテクニックとして使えると思います。
■同スコープ内での再宣言不可
varで変数宣言した場合は同スコープ(同関数内)で再度同じ名前で変数宣言してもエラーにはなりませんが、let/constでの変数宣言の場合は、同スコープ内(同ブロック内)での再宣言は出来ません。(babelコンパイルもエラーになります。)
var hoge = 'hoge'; var hoge = 'hoge2'; console.log(hoge); //hoge2 let fuga = 'fuga'; let fuga = 'fuga2'; //SyntaxError: Identifier 'fuga' has already been declared const piyo = 'piyo'; const piyo = 'piyo2'; //SyntaxError: Identifier 'piyo' has already been declared //let/constで同じ名前の変数を宣言することは出来ない let hogera = 'hogera'; const hogera = 'hogera'; //SyntaxError: Identifier 'hogera' has already been declared
再宣言が出来ないのはあくまでも「同スコープ上」に限るようです。例えば、以下の例の様に親スコープと同名の変数を宣言してもエラーはにはなりません。
const hoge = 'hoge'; { const hoge = 'hoge2'; //(※エラーにはならない) } console.log(hoge); //hoge
■グローバルオブジェクトのプロパティにはならない
例えば、JavaScriptの実行環境がブラウザだった場合、グローバルスコープの直下でvar宣言された変数はグローバルオブジェクト(windowオブジェクト)のプロパティになります。
let/constではグローバルスコープ直下で変数宣言をしても、グローバルオブジェクトにはプロパティが作成されません。
var hoge = 'hoge'; console.log(window.hoge); //hoge let fuga = 'fuga'; const piyo = 'piyo'; console.log(window.fuga); //undefined console.log(window.piyo); //undefined
■constは再代入も不可
letとconstの違いは再代入の可否です。letで宣言された変数は宣言時に渡された値を再代入で上書きすることが可能ですが、constで宣言された変数は宣言時の初期値に変更を加えることができません。(babelコンパイルもエラーになります。)
let hoge = 'hoge'; hoge = 'hogehoge'; console.log(hoge); // hogehoge const piyo = 'piyo'; piyo = 'piyopiyo'; //TypeError: Assignment to constant variable.
巻き上げ(ホイスティング)について
JavaScriptではあるスコープ内で宣言されたローカル変数は、すべてそのスコープの先頭で宣言されたものとみなされます。このことを変数の巻き上げ(ホイスティング)と言います。
(巻き上げについては別稿知らないと怖い「変数の巻き上げ」とは?に詳しくまとめてあります。)
varによる変数宣言の場合、同スコープ内にある宣言(または代入)文よりも前にその変数にアクセスしようとすると、undefinedが返されます。
対して、let/constでは同スコープ内にある宣言文よりも前にその変数にアクセスしようとすると ReferenceError が返されます。宣言文よりも前に代入しようとしても同様に ReferenceErrorになります。
「let/constでは巻き上げは発生しない」と書かれていることが多いようですが、厳密に言うと巻き上げは発生しています。ただ、undefinedではなくReferenceErrorを投げる点がvarとの違いです。let/constでも巻き上げが発生していることは次のコードで確認できます。
const hoge = 'hoge'; { console.log(hoge); //↑巻き上げが発生しないのであれば、ここで'hoge'が出力されるはずだが、 //宣言が巻き上げられているので、ReferenceErrorが返る const hoge = 'hoge2'; }
これらのことから、let/constを使う場合もvarの場合と同様、スコープの先頭で宣言するのが良いと言えそうですね。ただ、varの時よりもスコープを細かく区切れるので、スコープの先頭にもっさりとvar宣言が羅列される、、といったことにはならないでしょう。
'use strict'; (function () { console.log(hoge); //undefined var hoge = 'hoge'; console.log(hoge); //hoge }()); (function () { console.log(fuga); //ReferenceError: fuga is not defined let fuga = 'fuga'; console.log(fuga); }()); (function () { console.log(piyo); //ReferenceError: piyo is not defined const piyo = 'piyo'; console.log(piyo); }());
非strictモードでの挙動
let/constともに、ES2015の仕様的にはstrictモード/sloppyモード(非strictモード)に関わらず同一です。ただし、現時点ではブラウザの実装の問題上、sloppyモードでlet/constを使うとvarで宣言した場合と同じような挙動を示すようです。
最近ではstrictモードでコードを書くことが常識とされていますが、ES2015ベースでコードを書く際も引き続きstrictモード宣言はしておいた方が良さそうですね。