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
となります。