こんにちは、ほそ道です。
現在ほそ道が携わっているプロジェクトチームではES6コードで開発をしておりまして、
変数定義はlet
、const
を使用しております。var
は使いません。
さらにlet
は撲滅していき、すべてconst
化していく方向性を共有しています。
今回は、let
、const
の挙動と、なぜconst
化するのか、const
の盲点について。
次回以降はどうconst
化していくかのレシピを何回かに分けて紹介していきたいと思います。
let
/const
の挙動について
var
とlet
/const
の比較
重複定義
まずはvarについて。
var x = 100;
var x = 200;
console.log(x); // 200
上の例では同じ名前の変数、xを2度定義しています。
これは問題なく動作し、上の定義を下の定義が上書きするような形になります。
つづいて、let
/const
の場合を見ていきます。
let x = 100;
let x = 200; // SyntaxError: Identifier 'x' has already been declared
const x = 100;
const x = 200; // SyntaxError: Identifier 'x' has already been declared
重複定義はエラーになります。
ちなみにbabelコンパイルの場合はコンパイル段階でエラーとなり、Duplicate declaration "x"
のように表示され、そもそもコンパイルが通らなくなります。
スコープについて
まずは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
はどうでしょうか。
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 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;
}
let
もconst
も同一スコープ内に変数宣言がある場合、その変数名を手前でアクセスするとReferenceError
となるようです。
変数はスコープの先頭で定義すべし、という巻き上げバグ回避の掟はES6化後も守り続けるのが良さそうです。
とはいえ、ブロックスコープが作れることで変数定義が先頭過多になる場合を回避できるのはメリットと言えると思います。
let
とconst
の比較
上で挙げた、重複定義の非許容、スコープ判定、巻き上げについてはlet
/const
ともに同様の動きをします。
では今度はlet
とconst
の違いを見ていきます。
再代入
let
とconst
の違いは値の再代入に現れます。
まずはlet
から見ていきます。
let x = 100;
x = 200;
console.log(x); // 200
再代入は可能です。
続いて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 o = {x: 100};
o.x = 200;
o.y = 300;
console.log(o); // { x: 200, y: 300 }
既存要素への再代入、新規要素の追加、いずれもエラーは起こらず、値の追加・変更は成功してしまいます。
配列要素の変更
配列の破壊的メソッドについても同様のことが起こります。
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
に逃げてしまった」ケースを解決すると、結果的に可読性が高まることが多く個人的にワクワクしてしまいます。
長ーくなってしまうので、今回はここまでとします。
次回以降、盲点で紹介したケースについての解決レシピをいろいろと紹介していきたいと思ってます。
以上です!