JavaScriptの直感的に理解しづらい挙動として、巻き上げ(hoisting)があります。しかも、ES6で加わったconstやletは、従来と一味違った挙動を示します。
ES5での巻き上げ
ES5で宣言できるものとして、varによる変数の宣言と、functionによる関数宣言がありますが、この2つは巻き上げで挙動が違ってきます。
var
varは関数スコープですが、変数宣言だけ関数トップにあるものとして処理され、初期値の代入は元の位置で実行されます。
// 区別のために、同じ変数を外側でも宣言
var i = 7;
function foo(){
// var i; はここにあるのと同じ扱いに
console.log(i); // undefined
var i = 'hello';
console.log(i); // hello
}
console.log(i); // 7
foo();
function
functionで書き始める関数宣言の場合、スコープの中ならどこでも呼ぶことができます。varのような「名前だけあってundefined」ということにはなりません。もちろん、var f = function(){...};のような関数式の場合は、代入より前に呼ぶことはできません。
function test(){
inner1(); // 呼べる
inner2(); // 呼べない
function inner1(){
console.log('inner1');
}
var inner2 = function(){
console.log('inner2');
};
}
ES6では
ES6でも、従来のvarやfunctionの挙動は変わりませんが、新しく加わったletやconstは挙動が違ってきます(なお、どちらもスコープに関する挙動は同じです)。
これらはよく「巻き上げがない」と言われますが、(それを巻き上げと呼ぶかどうかは別として)宣言前の同一スコープにも影響します。では、先程のvarの例をletにしてみましょう。
// 外側はvarでもletでも同じだけど、気分的に
let i = 7;
function foo(){
console.log(i); // さあ、どうなる?
let i = 'hello';
console.log(i); // hello
}
console.log(i); // 7
foo();
「さあ、どうなる?」の部分は、letの宣言が上に影響しないなら、外側の7になると思うかもしれませんが、実際には**ReferenceError**となってしまいます。このように、代入前にletやconstの変数をアクセスしてReferenceErrorとなるのは、Temporal Dead Zoneと呼ばれます(MDN)。なお、本来エラーになるものだからか、Babelで正確にコンパイルされないこともあるようなので、お気をつけください。
あと、あくまでReferenceErrorとなるかは「代入と参照のコードの実行タイミング」によるもので、「コードの記載位置」とは別物です。少し前に @raccy さんの記事でのやり取りで知った話ですが、
const f = () => {
console.log("call f");
g(); // 後から定義する関数を書いても問題が無い。
};
const g = () => {
console.log("call g");
};
// main
f(); // 最後に実行する。
のように、あとから定義される変数を参照しても、宣言が実行されてからしか呼ばれない関数の中にあるのであれば、特に問題はありません。逆に、function()の巻き上げを使うなどして、constより下に書いた関数をconstの実行前に呼べばReferenceErrorとなります。