letやconstのホイスティングについて(javascript)

JavaScript

functionやvarの宣言はホイストされることは知っていましたが、let, const, class等の宣言はホイストされないと思い込んでいました。が、間違いだったようです。
知らず知らずのうちにホイストされる前提のコードを書いていて、後から見直してなぜこれで動いてるんだ?っとなったのでメモしておきます。

非常に参考になるページです。jsのホイストに関する疑問をお持ちの方は一度読んでみて下さい。
あーそういうことかぁ、と納得できると思います。

Hoisting in Modern JavaScript — let, const, and var
How Hoisting Really Works in JavaScript

以下は私なりの理解になります。

javascriptはざっくりcompile phaseとexecution phaseがあります。
compile phaseでjsファイルを読み込んで、必要に応じて初期化します。
execution phaseではjsファイル内のコードが順に実行されていきます。

このcompile phaseを通った時に、js内の変数はそれがvarであろうとlet, constであろうと、一旦目録としてまとめられます。
この際に、var宣言されているとundefinedで初期化されます。
一方でlet, const, class等は初期化すらされません。

var a;
let b;
const c;

というjsがあるとして、compile phaseでは以下のような目録が作られます。

目録 = {
     a: undefined
     b:
 <not initialized!>
     c:
 <not initialized!>
}

なので実行時に宣言前にconsole.log(a)としてもundefinedが出力されます。
console.log(b)とすると、bは目録上にはありますが初期化すらされてないのでb is not definedとエラーになります。

console.log(a) // => undefined
var a;

console.log(b) // => ERROR! b is not defined!
let b;

でもこれ少しだけ違和感があって、実際にコードを書いていると上のような露骨な間違いをすることはほとんどなく、letで宣言してもundefinedが入ってるような気がしませんか?

let b;
console.log(b); // => undefined

このコードは実際にbにundefinedが入っています。
じゃあいつundefinedで初期化されたのでしょう?

答えはlet b;の宣言の実行時です。
実装の中身は知りませんが、execution phaseで let b; を実行する際、目録を参照して初期化されていなければundefinedで初期化するようです。

ではconstではどうなるでしょう?

const c;  // => ERROR! Missing initializer in const declaration
console.log(c);

constは再代入できないため、宣言時に初期化されていないと即エラーとなります。
とはいえ目録上には変数cは登録されているため、実行さえしなければホイストされた変数cを使うことができます。

const hoge = { constVariable: c };
const c = 'fuga';
console.log(hoge.constVariable);

当然ですが{ constVariable: c }で変数cを宣言前に使っています。
目録上のcは初期化すらされていません。なので実行時エラーとなります。
(Cannot access 'c' before initialization)

しかし、以下は実行時にエラーにもならず、正当な書き方になります。
const hoge = () => {
    console.log(c);
};
const c = 'fuga';
hoge(); // => fuga

これはconst hogeの宣言時に変数cを使っているように見えますが、あくまで宣言だけでメソッドの中身が実行されるわけではありません。つまり変数cへの参照さえあれば成立するわけです。変数cへの参照は目録上にホイストされて作成済みであるため、なんの問題もありません。初期化すらされてませんが、実際にアクセスするまではエラーとして発現しません。
その後、実際にhoge()を実行する時までにconst cが初期化されていれば、目録上の変数cも初期化されるのでエラーとならず正常に実行できます。

逆に以下のように書いてしまうとエラーとなります。
(Cannot access 'c' before initialization)
const hoge = () => {
    console.log(c);
};
hoge(); // => fuga
const c = 'fuga';

let と constはこのようにホイストされて目録に登録されてから初期化されるまでにラグが発生します。これはTemporal Dead Zoneと呼ばれ、この間に変数にアクセスすると上で見たようなRefernceErrorとなります。

長々と書きましたが、let / constも実はホイストされてるよ、ってことです。
ただし、初期化のタイミングがずれるのでホイスト頼みで書くと無用のバグを埋め込むので、使い方はよく考えましょう。

コメント