--- tags: JavaScript, hexo部落格 title: 提升 Hosting --- # 提升 Hosting 先看看以下這些例子,會產生甚麼結果? e.g. 1 : console.log 結果會是? ```javascript console.log(a); // ? ``` e.g. 2 : console.log 結果會是? ```javascript console.log(a); var a = 1; // ? ``` e.g. 3 : fn() 是否執行? 又會產生甚麼? ```javascript fn(); function fn(){ retrun 1; } // ? ``` e.g. 4 : console.log 結果會是? ```javascript console.log(fn); function fn(){}; var fn = 1; // ? ``` --- ## 變數提升 當我們宣告變數時,JS 就會先開一個記憶體位置來存放,而它的預設值為 `undefined`,最後賦予我們給這個變數的值。而 JS 的執行順序為由上至下且依序執行,所以範例 1 與範例 2 就會是 : e.g. 1 : console.log 結果會是 `a is not defined` ```javascript console.log(a); ``` e.g. 2 : console.log 結果會是 `undefined` ```javascript console.log(a); var a = 1; ``` 實際上它們的執行順序是這樣 : ```javascript var a; console.log(a); a = 1; ``` ## 函式提升 在 JS中,它會依據函式定義的位置順序,依序將函式提升到最上方,但在函式內的不會移動到外層。 e.g. 3 : fn() 會執行並 return 1 ```javascript fn(); function fn(){ retrun 1; } // ? ``` 實際上它的執行順序是這樣 : ```javascript function fn(){ retrun 1; } fn(); ``` 而在函式內的函式不會提升到外層,但是可以呼叫外層的函式 : ```javascript var fnA = () => { fnB(); fnC(); var fnB = () => 2 } var fnC = () => 3 fnA(); ``` 不過,如果將匿名函式賦予到一個變數上,就會根據變數的規則走,而函式提升的優點在於,可以呼叫其他函式,不受執行順序影響,但執行時機會。 ## 變數與函式提升的優先程度 在 JS 中,會將優先將函式提升到最上方,接著再宣告變數,而函式的參數傳遞限於函式內並且是最優先,重新宣告它無任何意義。 e.g. 4 : console.log 結果會是 `function fn(){}` ```javascript console.log(fn) // 第一次 console.log(fn) 為 function fn(){} function fn(){} var fn = 1 console.log(fn) // 第二次 console.log(fn) 為 1 ``` ```javascript function fnA(a){ console.log(a) fnB(10) function fnB(a){ console.log(a+3) } var a = 5 } fnA(1) // 1 // 13 // fnB() 的參數其實跟 fnA 的參數 a 無關,只看 fnB() 真正執行時傳了甚麼值進去 // 若使用 let 宣告 a 會報錯,因為 a 為參數 // Identifier 'a' has already been declared ``` 再舉一個例子看函式是否提升到最上面 : ```javascript function fn() { a(); b(); var x = 2; var a = () => { x = 100; console.log(x) }; function b() { x = 200 console.log(x) } } fn(); // 執行匿名函式 a 會報錯,因為它還未被賦予值,所以一開始就執行 a() 會是 undefined // 執行函式 b 可以正常運行,因為它是函式且被提升最上面 ``` ## Hosting 做了甚麼事 我們打開編輯器撰寫 JS 時,其實是在一個全域環境底下開始寫程式,它同時是全域也是執行環境,當我們宣告一個 function 時,也會同時建立 : - 獨立的執行環境 ( Execution Contexts ) - 獨立的變數物件 ( Variable Object ) 以下的例子有不同情況 : 1. 未傳值的參數,其預設為 undefined ```javascript // 建立執行環境 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. 函式內建立函式,函式若與參數同名會被覆蓋 ```javascript // 執行環境 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. 總合以上情況並再次宣告參數然後賦予值 ```javascript 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 就是在建立變數物件的過程。** ```javascript // 撰寫 > 創建(包含建立變數物件) > 執行 console.log(a) var a = 1 console.log(a) // undefined // 1 ``` ## Temporal Dead Zone 目前的範例都是使用 var 來宣告,ES6開始就推薦以 let 或 const 來宣告變數,那麼 let 與 const 所宣告的變數與函式也具有提升特性嗎? ```javascript console.log(a) let a = 1 // Uncaught ReferenceError: Cannot access 'a' before initialization ``` 上面錯誤的意義是,未初始化變數 a 前,無法存取它的值,為什麼會這樣? ```javascript // 執行環境 console.log(a) let a = 1 // VO,在變數物件中還是有建立名稱, // 但按照依序執行的規則變成了未賦予值 // { // a: ? // } ``` 為了解釋這個空窗期,於是就有了這個名詞 Temporal Dead Zone。拋出錯誤的目的還是希望開發者的撰寫習慣能改變吧?! ## 參考來源 > 1. [Huli - 我知道你懂 hoisting,可是你了解到多深?](https://blog.techbridge.cc/2018/11/10/javascript-hoisting/) > 2. [ShawnL - JavaScript 深入淺出 Variable Object & Activation Object](https://shawnlin0201.github.io/JavaScript/JavaScript-Variable-Object-and-Activation-Object/) > 3. [江江好 - Day22【ES6 小筆記】變數提升(Hoisting)與暫時死區(TDZ)](https://ithelp.ithome.com.tw/articles/10219518)