ES6のconstを使い倒すレシピ1 - 前提共有編 〜 JSおくのほそ道 #034

こんにちは、ほそ道です。

現在ほそ道が携わっているプロジェクトチームではES6コードで開発をしておりまして、
変数定義は`let`、`const`を使用しております。`var`は使いません。
さらに`let`は撲滅していき、すべて`const`化していく方向性を共有しています。
今回は、`let`、`const`の挙動と、なぜ`const`化するのか、`const`の盲点について。
次回以降はどう`const`化していくかのレシピを何回かに分けて紹介していきたいと思います。

- [2.Object.freeze編](http://qiita.com/hosomichi/items/84b05c1b0c09d26cd11e)
- [3.immutable.js編](http://qiita.com/hosomichi/items/ea74d44efd47877b16a5)
- [4.コレクション一発代入/一発返却編](http://qiita.com/hosomichi/items/843d1d4ec72ccc6691de)
- [5.条件分岐一発代入/一発返却編](http://qiita.com/hosomichi/items/119dc53980c8f61a0485)

[目次はこちら](http://qiita.com/hosomichi/items/ad9a9c99a4fda041a563)

# `let`/`const`の挙動について

## `var`と`let`/`const`の比較

### 重複定義

まずはvarについて。

```js:varの重複定義
var x = 100;
var x = 200;

console.log(x);  // 200
```

上の例では同じ名前の変数、xを２度定義しています。
これは問題なく動作し、上の定義を下の定義が上書きするような形になります。

つづいて、`let`/`const`の場合を見ていきます。

```js:letを重複定義
let x = 100;
let x = 200;  // SyntaxError: Identifier 'x' has already been declared
```

```js:constを重複定義
const x = 100;
const x = 200;  // SyntaxError: Identifier 'x' has already been declared
```

重複定義はエラーになります。
ちなみにbabelコンパイルの場合はコンパイル段階でエラーとなり、`Duplicate declaration "x"`のように表示され、そもそもコンパイルが通らなくなります。

### スコープについて

まずはvarがどのようにスコープを認識するかから見ていきます。

```js:varのスコープ判定
var x = 100;

{
  var x = 200;   // ブロックスコープは無い
}

console.log(x);  // 200

if (true) {
  var x = 300;   // if/for/whileなども独自のスコープは無い
}

console.log(x);  // 300

(function() {
  var x = 400;   // 関数だけは独自のスコープを作り出せる
}());

console.log(x);  // 300
```

`var`は関数スコープ以外はプライベートスコープとみなしません。
では`let`や`const`はどうでしょうか。

```js:let/constのスコープ判定
let x = 100;

{
  let x = 200;   // ブロックスコープがある
}

console.log(x);  // 100 

if (true) {
  let x = 300;   // if/for/whileなどが独自のスコープを作る
}

console.log(x);  // 100 

(function() {
  let x = 400;   // 関数もやっぱりスコープを作る
}());

console.log(x);  // 100 
```

対して、`let`は関数以外の構文でもプライベートなスコープ空間とみなします。
`const`版は省略してますが`const`も同じ挙動となります。

`switch`文に関しては[ES6仕様](http://www.ecma-international.org/ecma-262/6.0/#sec-switch-statement-runtime-semantics-evaluation)ではコードブロック内で新しい宣言環境が発生するとあるのですが、現在Node5.0.0やbabel6.1.2コンパイルで環境で実行すると、独自スコープとはならないようです。

```js:let/constのswitch文のスコープ判定
let x = 100;

switch (true) {
  default:
    let x = 200;  // SyntaxError: Identifier 'x' has already been declared
}
```

### 巻き上げについて

スコープの先頭において、スコープ内で宣言する変数の初期化が行われる、巻き上げの動作はどうでしょうか。

```js:巻き上げ
{
    console.log(x); // ReferenceError: x is not defined
    let x = 200;
}
```

`let`も`const`も同一スコープ内に変数宣言がある場合、その変数名を手前でアクセスすると`ReferenceError`となるようです。
変数はスコープの先頭で定義すべし、という巻き上げバグ回避の掟はES6化後も守り続けるのが良さそうです。
とはいえ、ブロックスコープが作れることで変数定義が先頭過多になる場合を回避できるのはメリットと言えると思います。

## `let`と`const`の比較

上で挙げた、重複定義の非許容、スコープ判定、巻き上げについては`let`/`const`ともに同様の動きをします。
では今度は`let`と`const`の違いを見ていきます。

### 再代入

`let`と`const`の違いは値の再代入に現れます。
まずは`let`から見ていきます。

```js:letの再代入
let x = 100;
x = 200;

console.log(x);  // 200
```

再代入は可能です。

続いて`const`を見てみます。


```js:constの再代入
const x = 100;
x = 200;         // TypeError: Assignment to constant variable.
```

値の再代入は許容されず、エラーとなります。
babelコンパイル時にもコンパイルエラーとなり`"x" is read-only`などといったエラーが表示されます。

# なぜ`let`ではなく`const`を使うのか

## 値の状態管理
ある程度の規模のアプリケーションを開発する上で、
たとえば`let x = 100`のように宣言したとして、
そのxが色々な場所で書き換えられていると「いまxの中身はどうなってるんだっけ？」
という疑問が生まれたらコードを追いかけたり、デバッグ実行したりする必要が出てきます。
この場合、`const x = 100`という宣言にしておけば、宣言時以降xの値は100で保証されます。
xは状態の変化を持たない値となるということですね。
`const`化する利点としては宣言部のコードを確認すれば、開発を進める際に状態に迷うことがなくなる、コードデバッグ時の懸念事項が減少する、ということに尽きると思います。

# `const`の盲点
では`const`化すれば、あらゆる値の状態変化がないことを保証できるかというと、そうではありません。
const宣言の挙動としては「再代入を許容しない」ことにあります。再代入以外は許容します。

## オブジェクトの変更
`const`とオブジェクトの代入について見ていきましょう。

```js:オブジェクトの再代入
const o = {x: 100};
o = {y: 200};       // TypeError: Assignment to constant variable.
```

当然、エラーが発生します。
では子要素に対してはどうでしょうか。

```js:オブジェクト要素とconst
const o = {x: 100};
o.x = 200;
o.y = 300;

console.log(o);     // { x: 200, y: 300 }
```

既存要素への再代入、新規要素の追加、いずれもエラーは起こらず、値の追加・変更は成功してしまいます。

## 配列要素の変更

配列の破壊的メソッドについても同様のことが起こります。

```js:Arrayの破壊的メソッド
const a = [100];
a.push(200);

console.log(a);     // [ 100, 200 ]

a.shift();

console.log(a);     // [ 200 ]
```

中身がすっかり入れ替わってしまいました。
やはり状態は変更されてしまいます。

## プログラム実行過程でオブジェクトを形成したい場合

宣言部で状態を確定させたくても方法が見つからない。というケースもあるかと思います。
たとえば配列から偶数要素のみを抽出した配列を返す関数を作る場合を見ていきます。

```js:偶数だけを返す配列の定義
const getEvens = nums => {
  let i = 0, evens = [];
  for (; i < nums.length; i++) {
    if (nums[i] % 2 === 0) {
      evens.push(nums[i]);
    }
  }
  return evens;
}

console.log(getEvens([1,2,3,4,5,6]));
```

上記のように、不本意ながら`let`を使ってしまうケースもあるのではないでしょうか。
こういう「`let`に逃げてしまった」ケースを解決すると、結果的に可読性が高まることが多く個人的にワクワクしてしまいます。

---

長ーくなってしまうので、今回はここまでとします。
次回以降、盲点で紹介したケースについての解決レシピをいろいろと紹介していきたいと思ってます。
以上です！