概要
変数宣言について、
「変数はvarかlet、定数はconst。」
「varはもう使うな。」
とか聞いたことあるけど、詳細を理解していないので調べてみました。
違いを理解するための前提知識としてスコープの理解が必要だったので、それも調べてみました。
変数宣言
変数の宣言とは、変数名を登録し値を格納するための領域をメモリに確保することです。
JavaScriptでは変数を使う際、変数の宣言が推奨されています。(※宣言しなくても動くが注意が必要。詳細はおまけ1で)
変数宣言をするための命令として以下3つがあります。
let
とconst
はES2015から登場した新しい命令です。
- var
- let
- const
//宣言
var test;
console.log(test); //->代入していないのでundefined
//代入
test = 1;
//宣言時に代入してもOK(初期化という)
var test2 = 2;
//宣言せず代入することもできる
test3 = 3;
3つの違いは?
まとめるとこんなかんじです。
var | let | const | |
---|---|---|---|
再代入 | 〇 | 〇 | × |
再宣言 | 〇 | × | × |
ブロック内で変数を宣言した場合のスコープ | ブロックではない | ブロック | ブロック |
変数の巻き上げ | undefined | ReferenceError | ReferenceError |
それぞれ詳しくみていきましょう。 |
再代入
再代入とは、宣言した変数名に対して別の値を代入することです。
var
,let
は再代入できますが、const
はできないです。
「定数の宣言ではconst
を使う」とよく聞きますよね。これはconst
が再代入を許可しないからです。
//OK
var a = 'hoge';
a = 'fuga';
//OK
let b = 'hoge';
b = 'fuga';
//NG
const c = 'hoge';
c = 'fuga'; // ->TypeError: Assignment to constant variable.
再宣言
再宣言とは、既に宣言している変数名で再度変数の宣言を行うことです。
var
は再宣言ができますが、let
,const
はできないです。
再宣言できてしまうと、意図せず同じ変数名で宣言してしまったことで不具合につながる可能性があります。
var
を使う場合要注意ですね。
//OK
var a = 'hoge';
var a = 'fuga';
//NG
let b = 'hoge';
let b = 'fuga'; ->Identifier 'b' has already been declared
//NG
const c = 'hoge';
const c = 'fuga'; ->Identifier 'c' has already been declared
ブロック内で変数を宣言した場合のスコープ
ブロック内で変数の宣言を行った場合、var
,let
,const
のどれを使ったかで変数のスコープが変わります。
var
はブロックの外側のスコープになり、(関数内で宣言したならローカルスコープ、トップレベルで宣言したならグローバルスコープ)、
let
,const
はブロックスコープになります。
詳細にいく前に、スコープについて整理します。
スコープとは?
スコープとは、変数が参照できる範囲を決めるの概念のことです。
JavaScriptにおいてスコープには以下があります。
- グローバルスコープ
- ローカルスコープ
- ブロックスコープ
グローバルスコープ
スクリプト全体から参照が可能。グローバルスコープを持つ変数をグローバル変数という。
ローカルスコープ(関数スコープ)
宣言された関数内でのみ参照できる。(本によっては関数スコープ書いてる場合もあり。)ローカルスコープを持つ変数をローカル変数という。
function test(arg) {
// ローカル変数を宣言
var a = 'local';
console.log(a); // -> local
// 関数の仮引数もローカルスコープをもつ
console.log(arg); // -> hoge
}
test('hoge');
// 関数の外側からはローカル変数や仮引数を参照できない
console.log(a); // -> Uncaught ReferenceError: a is not defined
console.log(arg); // -> Uncaught ReferenceError: arg is not defined
ブロックスコープ
ES2015から追加されたスコープです。ブロック({})の範囲のみ参照できます。
例)if文やループ
function test() {
if (true){
// ブロック内で変数を宣言(ブロックスコープ)
let a = 'test';
}
// ブロックスコープの外側からは参照できない。
console.log(a); // ->ReferenceError: a is not defined
}
test();
var,let,constでブロックスコープの扱いが違う
スコープについて整理したところで、var
,let
,const
の違いに話を戻します。
ブロックスコープはES2015から追加された概念なので、let
,const
は対応していますが、ES2015以前からあったvar
は対応していないです。
function test() {
if (true){
// ブロック内で変数を宣言
var var_test = 'var'; // ローカルスコープ
let let_test = 'let'; // ブロックスコープ
const const_test = 'const'; // ブロックスコープ
}
console.log(var_test); // ローカルスコープなので関数内から参照できる
console.log(let_test); // ->ReferenceError: let_test is not defined ブロックの外から参照できない
console.log(const_test); // ->ReferenceError: const_test is not defined ブロックの外から参照できない
}
test();
変数の巻き上げ
ローカルスコープの説明の中で以下のように記述しました。
宣言された関数内でのみ参照できる。
つまり、宣言された関数内であれば、宣言より前の行から参照できる ということです。どゆこと??
function test() {
console.log(a); // -> ???
var a = 'test';
}
test();
console.log(a);
の結果はどうなるでしょうか?まだ宣言されてないのでReferenceErrorが出そうに思えますが...
.
.
.
正解はundefined
です。
変数aはローカルスコープをもっており、関数内全体で参照可能です。2行目console.log(a)
の時点では変数aの宣言だけがされており、値の代入はされていないため、undefined
となります。
以下のように書いた場合と同じになります。
function test() {
var a;
console.log(a); // -> undefined
a = 'test';
}
test();
このように、変数の宣言がスコープの先頭で行われたようになる挙動を「巻き上げ」または「ホイスティング」といいます。
ここまでの例では宣言にvar
を使ってきましたが、let
const
だとどうなるでしょうか?
function test() {
console.log(a); // -> ReferenceError: Cannot access 'a' before initialization
let a = 'test';
}
test();
エラーになります。const
でも同様にエラーになります。
let
,const
でも巻き上げは起こっていますが、値が代入される前に参照しようとするとReferenceErrorになります。
var
を使うときは要注意です。var
を使うなら、変数の宣言はスコープの先頭で行うべきですね。
##まとめ
再度、それぞれの違いをまとめます。
var | let | const | |
---|---|---|---|
再代入 | 〇 | 〇 | × |
再宣言 | 〇 | × | × |
ブロック内で変数を宣言した場合のスコープ | ブロックではない | ブロック | ブロック |
変数の巻き上げ | undefined | ReferenceError | ReferenceError |
おまけ1 ~変数宣言しなかったらどうなる?~
以下のコードでは、関数内の変数a
は宣言せず、いきなり代入しています。
宣言しなくても動くので、これ自体は問題ないです。
では、console.log(a)
の結果はどうなるでしょうか?
var a = 'global';
function test(){
a = 'local';
}
test();
console.log(a);
.
.
.
グローバル変数のa
を参照しているので、global
と言いたいところですが、結果はlocal
です。
var
let
const
を使って宣言しなかった変数はグローバル変数とみなされるからです。
var a = 'global';
function test(){
a = 'local'; // グローバル変数aに再代入
}
test();
console.log(a); // 再代入後のaを参照しているので、結果はlocalになる
「宣言しなくても動くんだから、しなくてもいいや」とか思ってると、意図せずグローバル変数を上書きしてしまいかねません。
原則変数宣言は行いましょう。
おまけ2 ~switch文でletを使うときは気を付けよう~
ブロック内でlet
で変数宣言した場合、スコープはブロックスコープになる、という話をしました。
ここで注意したいのがswitch文をつかった条件分岐です。
まず、if文を使ってこのように書いた場合、エラーにはなりません。
変数b
はブロックスコープを持つので、各条件のブロック内でのみ有効となるため、let b = 2;
は変数の再宣言になりません。
let a = 1;
// ここから
if (a === 1){
let b = 1;
}
// ここまでで1つのブロック
else if (a === 2){
let b = 2;
}
else{
let b = null;
}
これをswitch文を使って以下のように書くと、エラーになります。
switch文全体で1つのブロックになるので、let b = 2;
は変数の再宣言になります。
let a = 1;
// ここから
switch(a){
case 1:
let b = 1;
break;
case 2:
let b = 2; // -> SyntaxError: Identifier 'b' has already been declared
break;
default:
let b = null
break;
}
// ここまでで1つのブロック
switch文を使うなら、以下のように、switch文のブロックの外で変数b
を宣言すればOKです。
let a = 1;
// ブロック外で宣言
let b;
switch(a){
case 1:
b = 1;
break;
case 2:
b = 2;
break;
default:
b = null;
break;
}