JavaScriptのvar,let,constの違いを理解する。ついでにスコープも理解する。

## 概要
変数宣言について、
「変数はvarかlet、定数はconst。」
「varはもう使うな。」
とか聞いたことあるけど、詳細を理解していないので調べてみました。

違いを理解するための前提知識としてスコープの理解が必要だったので、それも調べてみました。


## 変数宣言
変数の宣言とは、変数名を登録し値を格納するための領域をメモリに確保することです。

JavaScriptでは変数を使う際、変数の宣言が推奨されています。（※宣言しなくても動くが注意が必要。詳細はおまけ1で）
変数宣言をするための命令として以下3つがあります。
`let`と`const` はES2015から登場した新しい命令です。

* var
* let
* const

```JavaScript
//宣言
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`が再代入を許可しないからです。


```JavaScript
//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`を使う場合要注意ですね。

```JavaScript
//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においてスコープには以下があります。

* グローバルスコープ
* ローカルスコープ
* ブロックスコープ

#### グローバルスコープ
スクリプト全体から参照が可能。グローバルスコープを持つ変数をグローバル変数という。

#### ローカルスコープ(関数スコープ)
宣言された関数内でのみ参照できる。(本によっては関数スコープ書いてる場合もあり。)ローカルスコープを持つ変数をローカル変数という。

```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文やループ

```JavaScript
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`は対応していないです。

```JavaScript
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();
```

### 変数の巻き上げ

ローカルスコープの説明の中で以下のように記述しました。

> 宣言された関数内でのみ参照できる。

つまり、宣言された関数内であれば、宣言より前の行から参照できる　ということです。どゆこと？？

```JavaScript
function test() {
  console.log(a); // -> ???
  var a = 'test';
}
test();
```
`console.log(a); `
の結果はどうなるでしょうか？まだ宣言されてないのでReferenceErrorが出そうに思えますが...
.
.
.
正解は`undefined`です。
変数aはローカルスコープをもっており、関数内全体で参照可能です。2行目`console.log(a)`の時点では変数aの宣言だけがされており、値の代入はされていないため、`undefined`となります。
以下のように書いた場合と同じになります。

```JavaScript
function test() {
  var a;
  console.log(a); // -> undefined
  a = 'test';
}
test();
```
このように、変数の宣言がスコープの先頭で行われたようになる挙動を「巻き上げ」または「ホイスティング」といいます。

ここまでの例では宣言に`var`を使ってきましたが、`let` `const`　だとどうなるでしょうか？

```JavaScript
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)`の結果はどうなるでしょうか？

```JavaScript
var a = 'global';
function test(){
    a = 'local';
}
test();
console.log(a);
```
.
.
.

グローバル変数の`a`を参照しているので、`global`と言いたいところですが、結果は`local`です。
`var` `let` `const` を使って宣言しなかった変数はグローバル変数とみなされるからです。


```JavaScript
var a = 'global';
function test(){
    a = 'local';  // グローバル変数aに再代入
}
test();
console.log(a); // 再代入後のaを参照しているので、結果はlocalになる
```
「宣言しなくても動くんだから、しなくてもいいや」とか思ってると、意図せずグローバル変数を上書きしてしまいかねません。
原則変数宣言は行いましょう。


## おまけ2　~switch文でletを使うときは気を付けよう~
ブロック内で`let`で変数宣言した場合、スコープはブロックスコープになる、という話をしました。
ここで注意したいのがswitch文をつかった条件分岐です。

まず、if文を使ってこのように書いた場合、エラーにはなりません。
変数`b`はブロックスコープを持つので、各条件のブロック内でのみ有効となるため、`let b = 2;` は変数の再宣言になりません。

```JavaScript
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;`は変数の再宣言になります。

```JavaScript
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です。

```JavaScript
let a = 1;
// ブロック外で宣言
let b;
switch(a){
    case 1:
        b = 1;
        break;
    case 2:
        b = 2; 
        break;
    default:
        b = null;
        break;
}
```