0%

提升

分別呼叫這些例子,會產生甚麼結果?

E.g. 1. : console.log 結果會是?

1
2
console.log(a)
// ?

E.g. 2 : console.log 結果會是?

1
2
3
console.log(a)
var a = 1
// ?

E.g. 3 : fnA() 是否執行? 又會產生甚麼?

1
2
3
fn()
var fn = () => 1
// ?

E.g. 4 : console.log 結果會是?

1
2
3
4
console.log(fn)
function fn(){}
var fn = 1
// ?

變數提升

當我們宣告變數時,JS就會先開一個記憶體位置來存放,而它的預設值為 ‘undefined’,最後賦予我們給這個變數的值。
而 JS的執行順序為由上至下且依序執行,所以範例1與範例2的樣子就會是 :
E.g. 1 : console.log 結果會是 a is not defined

1
console.log(a) 

E.g. 2 : console.log 結果會是 undefined

1
2
console.log(a)
var a = 1

實際上它的執行順序是這樣 :

1
2
3
var a
console.log(a)
a = 1

函式提升

在 JS中,它會依據函式定義的位置順序,依序將函式提升到最上方,但在函式內的不會移動到外層。
E.g. 3 : fnA() 會執行並 return 1

1
2
3
fn()
var fn = () => 1
// ?

實際上它的執行順序是這樣 :

1
2
var fn = () => 1
fn()

而在函式內的函式不會提升到外層,但是可以呼叫外層的函式 :

1
2
3
4
5
6
7
var fnA = () => {
fnB()
fnC()
var fnB = () => 2
}
var fnC = () => 3
fnA()

不過,如果將匿名函式賦予到一個變數上,就會根據變數的規則走。


變數與函式提升的優先程度

在 JS中,會將優先將函式提升到最上方,接著再宣告變數,而函式的參數傳遞限於函式內並且是最優先,重新宣告它無任何意義。
E.g. 4 : console.log 結果會是 function fn( ){}

1
2
3
4
5
6
console.log(fn)
// 第一次 console.log(fn) 為 function fn(){}
function fn(){}
var fn = 1
console.log(fn)
// 第二次 console.log(fn) 為 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fnA(a){
console.log(a)
fnB(10)
function fnB(a){
console.log(a+3)
}
var a = 5
}
fnA(1)
// 1
// 13
// 若使用 let 宣告 a會報錯,因為 a為參數
// Identifier 'a' has already been declared
// fnB() 的參數其實跟 fnA的參數 a無關,只看 fnB()真正執行時傳了甚麼值進去

Hosting 做了甚麼事

我們打開編輯器撰寫 JS時,其實是在一個全域環境底下開始寫程式,它同時是全域也是執行環境,當我們宣告一個 function時,也會同時建立 :

  • 獨立的執行環境 ( Execution Contexts )
  • 獨立的變數物件 ( Variable Object )

以下的例子有不同情況 :

  1. 多餘的變數,其預設為 undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 建立執行環境 Execution Contexts
    function fnA(a, b, c){
    console.log(a, b, c)
    }
    fnA(10)
    // 10 undefined undefined

    // 建立變數物件 VO
    // {
    // a:10
    // b:undefined
    // c:undefined
    // }
  2. 函式內建立函式,函式若與參數同名會被覆蓋

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 執行環境 Execution Contexts
    function fnA(a){
    function a(){}
    console.log(a)
    }
    function fnB(){return 1}
    fnA(fnB)
    // function a(){}

    // 參數 a 被函式覆蓋掉
    // VO variable object
    // {
    // a: function a(){}
    // }
  3. 總合以上情況並再次宣告參數然後賦予值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function fnA(a, b, c){
    function b(){}
    console.log(a, b, c)
    // 10 function b(){} undefined
    var c = 5
    console.log(c)
    // 5
    }
    function fnB(){return 1}
    fnA(10, fnB)

    // 為什麼 c不是 5? 因為它宣告被忽略且呼叫時未被設定成 5
    // 在第二次呼叫 c 時,已被設定成 5

    // VO variable object
    // {
    // a: 10,
    // b: function b(){},
    // c: undefined
    // }

透過以上例子可以發現,當有執行環境後,就會有變數物件,而它在函式中會做以下三件事 :

  • 若參數值為函式且參數名稱同名則覆蓋成函式
  • 若函式內宣告且與參數名稱同名則忽略
  • 參數名稱若有設定,預設值為 undefined

回到全域環境後,就更加單純,使用 var宣告的函式與變數會被覆蓋,若該變數未賦予值時,預設值為 undefined。

再回想一下,當我們寫好 JS程式碼後,透過瀏覽器執行,其實是開始創建接著執行,在創建(包含編譯)的過程中,Hosting 就是在建立變數物件的過程。


1
2
3
4
5
6
// 撰寫 > 創建(包含建立變數物件) > 執行
console.log(a)
var a = 1
console.log(a)
// undefined
// 1

Temporal Dead Zone

目前的範例都是使用 var 來宣告,ES6開始就推薦以 let或 const 來宣告變數,那麼 let 與 const 所宣告的變數與函式也具有提升特性嗎?

1
2
3
console.log(a)
let a = 1
// Uncaught ReferenceError: Cannot access 'a' before initialization

上面錯誤的意義是,未初始化變數 a前,無法存取它的值,為什麼會這樣?

1
2
3
4
5
6
7
8
// 執行環境
console.log(a)
let a = 1
// VO,在變數物件中還是有建立名稱,
// 但按照依序執行的規則變成了未賦予值
// {
// a: ?
// }

為了解釋這個空窗期,於是就有了這個名詞 Temporal Dead Zone。拋出錯誤的目的還是希望開發者的撰寫習慣能改變吧?!

參考來源

  1. Huli - 我知道你懂 hoisting,可是你了解到多深?
  2. ShawnL - JavaScript 深入淺出 Variable Object & Activation Object
  3. 江江好 - Day22【ES6 小筆記】變數提升(Hoisting)與暫時死區(TDZ)