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

More than 1 year has passed since last update.

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

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

目次はこちら

let/constの挙動について

varlet/constの比較

重複定義

まずはvarについて。

varの重複定義
var x = 100;
var x = 200;

console.log(x);  // 200

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

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

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

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

スコープについて

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

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は関数スコープ以外はプライベートスコープとみなしません。
ではletconstはどうでしょうか。

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仕様ではコードブロック内で新しい宣言環境が発生するとあるのですが、現在Node5.0.0やbabel6.1.2コンパイルで環境で実行すると、独自スコープとはならないようです。

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

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

巻き上げについて

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

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

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

letconstの比較

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

再代入

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

letの再代入
let x = 100;
x = 200;

console.log(x);  // 200

再代入は可能です。

続いてconstを見てみます。

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とオブジェクトの代入について見ていきましょう。

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

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

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

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

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

配列要素の変更

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

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

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

a.shift();

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

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

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

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

偶数だけを返す配列の定義
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に逃げてしまった」ケースを解決すると、結果的に可読性が高まることが多く個人的にワクワクしてしまいます。


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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.