初めに
たくさんの資料調べるうちに数か月前自分の書いた文章を振り返ると、自分の理解があやふやになっていることに気づき、基礎概念ともう一度向き合って文章にしたいと思います。
シリーズ参考資料はこちらです↓
You Don't Know JS (book series)
今回扱うのは
You Don't Know JS: Up & Going - Chapter 2: Into JavaScript
You Don't Know JS: Scope & Closures - Chapter 4: Hoisting
Kinds of Emptiness
の勉強メモです。
07/04/22編集:
元の文章ではundefined
を状態として理解していましたが、正しくは値です。
Hoisting
var
Hoistingは、var
などを使って変数宣言するとき、その変数が自分のいるスコープ一番上に巻き上げていくことです。
console.log(a) // undefined
// undefined means no value
// we declared it but we doesn't assign any value
var a // compile-time
a = 2 // run-time
console.log(a) // 2
var
を使えばcompile-time(parsing)
でのhoistingが必ず発生します。
しかしhoisitingの対象は変数です。値ではありません。
変数がrun-time(executing)
で値を付与されるまでundefined
(値)で初期化されている。(initialize as undefined)
example1
実際の順序は、
var a
← compile-timeでは順序に関わらず、一番上に置くのがhoisitingです。
console.log(a) // undefined
a = 2
← run-timeでコードを実行するとき値を付与する。
console.log(a) // 2
compile-time
というのは、コード執行する前に、JavaScriptエンジンはScope
でcompilation
を行ってコード解析し機械言語に変換する段階です。
同じ作者のシリーズ本ではこちらのYou Don't Know JS: Scope & Closures - 1st Edition扱っています。
少し前にスコープについて自分なりにまとめましたが、ご参考になればと。↓
JavaScriptのクロージャについて - Scope
foo() // undefined
function foo() {
console.log(a)
}
console.log(a) // undefined
var a // compile-time
foo() // undefined
a = 1 // run-time
console.log(a) // 1
foo() // 1
example2
の順序は、
var a
function foo() {console.log(a)}
foo() // undefined
*一行目の呼び出し
console.log(a) // undefined
foo() // undefined
a = 1
console.log(a) // 1
foo() // 1
function
宣言ももちろんhoisitingが発生します。
でもなぜvar
のhoisitingがfunction
より前だと言えるでしょうか?
まずvar a
を消してfoo()
だけを呼び出したらReferenceError: a is not defined
になります。
そして一行目のfoo()
もconsole.log(a)
もundefined
を出力することから、var a
の後function foo() {...}
ではないとfoo()
は実行できないはずです。(ASTにも関連している気がしますが)
それからfunction foo() {...}
、console.log(a)
...が続いていくということがわかります。そしてa = 1
run-timeで値が付与されて初めてa
の値を出力することが可能になったんです。
(function
はどこで書いてもトップにあがりますが、順序としてはvar
のような関数宣言より下、ネストした関数(内部関数)を除いてどこで呼び出されても可能です。)
Variable can't be hoisted in function scope?
global scope
もfunction scope
も、var
での宣言ではhoistingが必ず発生します。
function outer() {
console.log(a) // undefined
var a
console.log(a) // undefined
a = 2
console.log(a) // 2
}
outer()
では、なぜ下の例では、上のtest2()
が出力できなかったでしょうか...
function outer() {
test1() // I am test1
// test2() // TypeError: test2 is not a function
function test1() {
console.log('I am test1')
}
var test2 = function () {
console.log('I am test2')
}
test2() // I am test2
}
outer()
やはりhoisting作用の対象が誤解されやすいところだと思います。
outer()
内部の順序、実際は
var test2
function test1() {...}
test()1
test()2 // TypeError
test2 = function () {...}
test2() // I am test2
となります。
hoistingは変数を巻き上げるけど、値は別の段階で付与されるから、test2
の値(function () {console.log('I am test2')}
)より先に呼び出されたら、
もちろんTypeError: test2 is not a function
が出てきます。
上のtest2()
ではtest2
はまだundefined
なので、undefined
を関数のように扱うことができません。
let/const
let
とconst
には、hoistingが発生しないのでしょうか?
発生します。 ここではTDZ(Temporal Dead Zone)
を紹介したいと思います。
TDZ(Temporal Dead Zone)
は一時死亡状態に指します。つまり何もできない状態です。
// Before execution = compile time
never been created 'let/const' 'var' declared
in any scope declared ex. var a
-------------------|-------------------|-------------------> time
undeclared TDZ undefined
(uninitailized)
var
宣言は変数をhoistingして、execution(run time、値が付与される)まではundefined
で初期化になる (initialize as undefined) ことに対し、
let/const
宣言も同じく変数をhoistingして、executionまで変数をuninitialized(初期化されてない)
、何も付与できない状態を保っているんです。
uninitialized
状態の値をアクセスしたらReferenceError: Cannot access before initialization
が出てきます。
そしてexecution 段階になってはじめてundefined
という初期値が付与される。
簡潔にいうと、
var
で変数宣言される時点(compile time)すぐundefined
という初期値がつく。executionで新しい値が付与されるまでundefined
です。
let/const
で変数宣言される時点(compile time)で何にもできない死亡している状態で、execution(run time) で初めてundefined
が付与され、もし値を与えていなければundefined
になります。
// x // ReferenceError: Cannot access 'x' before initialization
let x
console.log(x) // undefined
x = 3
console.log(x) // 3
結論
var
, const
, let
の違いを理解して、変数をちゃんと宣言してから使うこと。
ほかに
var
、let
、const
以外function
、function*
、class
、JavaScriptではすべての宣言がhoisted
状態になります。
こちらにご参考にしてください。↓
Are variables declared with let or const hoisted?