30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

var,let,constの違いは、ブロックスコープと巻き上げ

JavaScriptにおける変数宣言はvarによって行われてきたが、ES2015で、letconstが加わった。
varとの違いは、ブロックスコープを作るということと、変数の巻き上げの挙動が異なる、ということである。
このことについて、varletを比較することで説明していく。constについては最後に触れる。

ブロックスコープ

これまでのJavaScriptでは、スコープをつくるのは、

  1. グローバルスコープ
  2. ローカルスコープ
  3. evalスコープ

の3つのみであった。

スコープについては以下を参照。
スコープとクロージャ

{}で囲まれている領域をブロックと呼ぶが、これはスコープは作らなかった。そのため、ifforはスコープを持たなかった。

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の違い

基本的に、letconstは、同じ挙動をする。そのため、ここまで書いた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]])を変更する必要がある。
オブジェクトやプロパティの操作、内部属性については、以下の記事を参照。