JavaScriptにおける変数宣言はvar
によって行われてきたが、ES2015で、let
とconst
が加わった。
var
との違いは、ブロックスコープを作るということと、変数の巻き上げの挙動が異なる、ということである。
このことについて、var
とlet
を比較することで説明していく。const
については最後に触れる。
ブロックスコープ
これまでのJavaScriptでは、スコープをつくるのは、
- グローバルスコープ
- ローカルスコープ
eval
スコープ
の3つのみであった。
スコープについては以下を参照。
スコープとクロージャ
{}
で囲まれている領域をブロックと呼ぶが、これはスコープは作らなかった。そのため、if
やfor
はスコープを持たなかった。
var x = 0; console.log(x); // 0 if(true){ var x = 1; console.log(x); // 1 }; console.log(x); // 1 // ブロックスコープを持たないため、if文の内側のxも、外側のxも、同じものを指す
しかしlet
は、ブロックスコープを作る。
var x = 0; console.log(x); // 0 if(true){ let x = 1; console.log(x); // 1 }; console.log(x); // 0 // letはブロックスコープを持つため、if文の内側のxと外側のxは、区別される
このように、ブロックスコープを作るかどうかが、1つ目の違いである。
変数の巻き上げ
もう1つの違いは、変数の巻き上げの挙動である。
変数の巻き上げとは、スコープ内で宣言された変数は、実際に宣言された場所に関係なく、スコープの先頭で宣言されたことになる、というものである。
console.log(x); // undefined var x = 0;
console.log()
を実行した時点では変数x
は宣言されていないため、x is not defined
でエラーになりそうだが、ならない。
巻き上げが行われている、つまり、このコードの先頭でx
が宣言されていると見なされるためである。
しかし、巻き上げられるのは宣言のみであり、代入は行われない。代入はあくまでも、実際に代入が行われている部分で実行される。
つまり、先ほどのコードは、以下のコードと同じ挙動をする。
var x; console.log(x); // undefined var x = 0;
変数の巻き上げはスコープ毎に行われる。
以下のコードでは、myFunc
のスコープの先頭で、変数scope
を宣言している。そのため、スコープチェーンを辿ってThis is global.
を返すことはない。
var scope = 'This is global.'; var myFunc = function(){ console.log(scope); var scope = 'This is local.'; console.log(scope); }; myFunc(); // undefined // This is local.
だがlet
では、巻き上げの挙動が異なる。そのため以下のコードは、エラーになる。
console.log(x); // ReferenceError: x is not defined let x = 0;
var
のような巻き上げが行われていないことが分かる。
これだけ見るとlet
は変数の巻き上げを行わないように思えるが、そうではない。
「巻き上げを行うが参照することは出来ない」というのが、正解に近い。
以下のコードでそれが確認できる。
let scope = 'This is global.'; var myFunc = function(){ console.log(scope); }; var myFunc2 = function(){ console.log(scope); var scope = 'This is local.'; console.log(scope); }; var myFunc3 = function(){ console.log(scope); let scope = 'This is local.'; console.log(scope); }; myFunc(); // This is global. myFunc2(); // undefined // This is local. myFunc3(); // ReferenceError: scope is not defined
myFunc()
ではスコープチェーンが行われ、myFunc2()
ではvar
による変数の巻き上げが行われている。
問題はmyFunc3()
である。
変数の巻き上げが発生しないのであれば、最初のconsole.log()
ではスコープチェーンによってThis is global.
が返されるはずだが、実際には参照エラーになってしまっている。
つまり、巻き上げ自体は起こっているが、それを参照しようとするとエラーになる。
このページによれば、
ECMAScript 6 では let は変数をブロックの先頭へ引き上げます。しかし、その変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。
とのこと。
letとconstの違い
基本的に、let
とconst
は、同じ挙動をする。そのため、ここまで書いたlet
についての説明は、const
に置き換えても問題ない。
唯一の違いは、再代入が可能かどうか、である。
let
は再代入が可能であるが、const
は再代入が出来ない。
let x = 0; const y = 98; x = 1; y = 99; // TypeError: Assignment to constant variable.
但し、あくまでも「再代入」が不可能なだけで、そこに入っているオブジェクトや配列の操作は問題なく行える。
以下のコードでは、const
に代入されたオブジェクトobj
の中身が操作され、宣言時とは全く違う中身になっていることが分かる。
const obj = { prop1: 1 }; console.log(Object.isExtensible(obj)); // true console.log(Object.getOwnPropertyDescriptor(obj, 'prop1')); // { value: 1, writable: true, enumerable: true, configurable: true } // objは拡張可能、prop1は列挙や変更が可能であることが、確認できる obj.prop1 = 2; console.log(obj.prop1); // 2 obj.prop2 = 99; delete obj.prop1; console.log(obj); // { prop2: 99 }
これを防ぐためには、オブジェクトやプロパティの内部属性(例えば[[Extensible]]
)を変更する必要がある。
オブジェクトやプロパティの操作、内部属性については、以下の記事を参照。