深入淺出JS - 變數提升(函式宣告提升)

秋秋秋丷發表於2018-05-17

前言

在我們的日常工作中,變數無處不在。更加深入的去了解它,能夠使得自己的JS水平更上一層樓, 從變數提升這個小知識點著手,讓我們一起來深入瞭解JS吧!

變數提升的小栗子

console.log(a) // undefined
var a = 'hello JS' 

/* 在我們宣告a之前為什麼輸出a不會報錯呢? 不急,讓我們接著往下看 */

num = 6;
num++;
var num;
console.log(num) // 7 好奇怪,為什麼給一個還沒有宣告的變數賦值會不報錯呢

function hoistFunction() {
    foo();
    function foo() {        
        console.log('running...')    
    }
}
hoistFunction(); // running... 

/* 最後一個栗子 */

alert(a) //  function a { alert(10) }
a(); // 10
var a = 3;
function a() {    
    alert(10)
};
alert(a) // 3
a = 6;
a(); // throw error複製程式碼


分析原因

JS引擎會在正式執行程式碼之前進行一次”預編譯“,預編譯簡單理解就是在記憶體中開闢一些空間,存放一些變數和函式。具體步驟如下(browser):

  • 頁面建立GO全域性物件(Global Object)物件(window物件)。
  • 載入第一個指令碼檔案
  • 指令碼載入完畢後,進行語法分析。
  • 開始預編譯
    • 查詢函式宣告,作為GO屬性,值賦予函式體(函式宣告優先)
    • 查詢變數宣告,作為GO屬性,值賦予undefined
    • GO/window = {
          //頁面載入建立GO同時,建立了document、navigator、screen等等屬性,此處省略
          a: undefined,
          c: undefined,
          b: function(y){
              var x = 1;
              console.log('so easy');
          }
      }複製程式碼
  • 解釋執行程式碼(直到執行函式b,該部分也被叫做詞法分析
    • 建立AO活動物件(Active Object)
    • 查詢形參和變數宣告,值賦予undefined
    • 實參值賦給形參
    • 查詢函式宣告,值賦給函式體
    • 解釋執行函式中的程式碼

      GO/window = {
          //變數隨著執行流得到初始化
          a: 1,
          c: function(){
              //...
          },
          b: function(y){
              var x = 1;
              console.log('so easy');
          }
      }
      複製程式碼
  • 第一個指令碼檔案執行完畢,載入第二個指令碼檔案
  • 第二個檔案載入完畢後,進行語法分析
  • 開始預編譯
    • 重複預編譯步驟 ....

預解析機制使得變數提升(Hoisting),從字面上理解就是變數和函式的宣告會移動到移動到函式或者全域性程式碼的開頭位置。我們再來分析一下小栗子加深一下理解。

console.log(a) // 執行之前,變數提升作為window的屬性, 值被設定為undefined
var a = 'hello JS' 

/* JavaScript 僅提升宣告,而不提升初始化 */

num = 6;
num++;
var num;
console.log(num) // 變數提升 值為undefined的num賦值為6,再自增 => 7

function hoistFunction() {
    foo();
    function foo() {        
        console.log('running...')    
    }
}
hoistFunction(); // 函式宣告提升,可以在函式體之前執行

/* 最後一個栗子 */

alert(a) // 最後的宣告為函式宣告, 因此a此時為函式體
a(); // 執行 a 函式,輸出10
var a = 3; // 3 賦給a
function a() {    
    alert(10)
};
alert(a) // 3
a = 6; // 6賦給a,不是一個函式,故下方執行throw error
a(); // throw error
複製程式碼

注: JS並不存在真正的預編譯,var與function的提升實際是在語法分析階段就處理好的。而且JS的預編譯是以一個指令碼檔案為塊的。一個指令碼檔案進行一次預編譯,而不是全文編譯完成再進行”預編譯”的。


最佳實踐 

理解了變數提升和函式提升可以使得我們在JS上走的更遠,但是我們在開發中,不應該使用這一特性,而是要規範我們的程式碼,做到可維護性和可讀性。無論是變數還是函式,都必須先宣告後使用。PS:在開發中應該使用let來約束變數提升。


參考資料:  

https://developer.mozilla.org/zh-CN/docs/Glossary/Hoisting https://www.cnblogs.com/liuhe688/p/5891273.html http://dmitrysoshnikov.com/notes/note-4-two-words-about-hoisting/ https://segmentfault.com/a/1190000010187653





相關文章