JavaScript
関数
es6
変数

ES6時代の巻き上げ(hoisting)

More than 1 year has passed since last update.

JavaScriptの直感的に理解しづらい挙動として、巻き上げ(hoisting)があります。しかも、ES6で加わったconstletは、従来と一味違った挙動を示します。


ES5での巻き上げ

ES5で宣言できるものとして、varによる変数の宣言と、functionによる関数宣言がありますが、この2つは巻き上げで挙動が違ってきます。


var

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の巻き上げ

function test(){

inner1(); // 呼べる
inner2(); // 呼べない

function inner1(){
console.log('inner1');
}
var inner2 = function(){
console.log('inner2');
};
}



ES6では

ES6でも、従来のvarfunctionの挙動は変わりませんが、新しく加わったletconstは挙動が違ってきます(なお、どちらもスコープに関する挙動は同じです)。

これらはよく「巻き上げがない」と言われますが、(それを巻き上げと呼ぶかどうかは別として)宣言前の同一スコープにも影響します。では、先程のvarの例をletにしてみましょう。


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となってしまいます。このように、代入前にletconstの変数をアクセスして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となります。