你不知道的JavaScript--Item6 var預解析與函式宣告提升

itKingOne發表於2018-06-21

1、var 變數預編譯

JavaScript 的語法和 C 、Java、C# 類似,統稱為 C 類語法。有過 C 或 Java 程式設計經驗的同學應該對“先宣告、後使用”的規則很熟悉,如果使用未經宣告的變數或函式,在編譯階段就會報錯。然而,JavaScript 卻能夠在變數和函式被宣告之前使用它們。下面我們就深入瞭解一下其中的玄機。

先來看一段程式碼:

(function() {
  console.log(noSuchVariable);//ReferenceError: noSuchVariable is not defined
})();

執行上面程式碼立馬就報錯,不過,這也正是我們期望的,因為 noSuchVariable 變數根本就沒有定義過嘛!再來看看下面的程式碼:

(function() {
  console.log(declaredLater);  //undefined
  var declaredLater = "Now it's defined!";
  console.log(declaredLater);// "Now it's defined!"
})();

首先,上面這段程式碼是正確的,沒有任何問題。但是,為什麼不報錯了?declaredLater 變數是在呼叫語句後面定義的啊?為什麼居然輸出的是 undefined?

這其實是 JavaScript 解析器搞的鬼,解析器將當前作用域內宣告的所有變數和函式都會放到作用域的開始處,但是,只有變數的宣告被提前到作用域的開始處了,而賦值操作被保留在原處。上述程式碼對於解析器來說其實是如下這個樣子滴:

(function() {
  var declaredLater; //宣告被提前到作用域開始處了!
  console.log(declaredLater);  // undefined
  declaredLater = "Now it's defined!"; //賦值操作還在原地!
  console.log(declaredLater);//"Now it's defined!"
})();

這就是為什麼上述程式碼不報異常的原因!變數和函式經過“被提前”之後,declaredLater 變數其實就被放在了呼叫函式的前面,根據 JavaScript 語法的定義,已宣告而未被賦值的變數會被自動賦值為 undefined ,所以,第一次列印 declaredLater 變數的值就是 undefined,後面我們對 declaredLater 變數進行了賦值操作,所以,第二次再列印變數就會輸出Now it’s defined!。

再來看一個例子:

var name = "Baggins";
(function () {
    console.log("Original name was " + name);// "Original name was undefined"
    var name = "Underhill";
    console.log("New name is " + name);// "New name is Underhill"
})();

上述程式碼中,我們先宣告瞭一個變數 name ,我們的本意是希望在第一次列印 name 變數時能夠輸出全域性範圍內定義的 name 變數,然後再在函式中定義一個區域性 name 變數覆蓋全域性變數,最後輸出區域性變數的值。可是第一次輸出的結果和我們的預期完全不一致,原因就是我們定義的區域性變數在其作用域內被“提前”了,也就是變成了如下形式:

var name = "Baggins";
(function () {
    var name;  //注意:name 變數被提前了!
    console.log("Original name was " + name);// "Original name was undefined"
    name = "Underhill";
    console.log("New name is " + name);//"New name is Underhill"
})();

由於 JavaScript 具有這樣的“怪癖”,所以建議大家將變數宣告放在作用域的最上方,這樣就能時刻提醒自己注意了。

2、函式宣告“被提前”

前邊說的是變數,接下來我們說說函式。

函式的“被提前”還要分兩種情況,一種是函式宣告,第二種是函式作為值賦值給變數,也即函式表示式

先說第一種情況,上程式碼:

isItHoisted();//"Yes!"
function isItHoisted() {  
    console.log("Yes!");
}

如上所示,JavaScript 直譯器允許你在函式宣告之前使用,也就是說,函式宣告並不僅僅是函式名“被提前”了,整個函式的定義也“被提前”了!所以上述程式碼能夠正確執行。

再來看第二種情況:函式表示式形式。還是先上程式碼:

definitionHoisted();// "Definition hoisted!"
definitionNotHoisted();// TypeError: undefined is not a function
function definitionHoisted() {  
    console.log("Definition hoisted!");
}
var definitionNotHoisted = function () {  
    console.log("Definition not hoisted!");
};

我們做了一個對比,definitionHoisted 函式被妥妥的執行了,符合第一種型別;definitionNotHoisted 變數“被提前”了,但是他的賦值(也就是函式)並沒有被提前,從這一點上來說,和前面我們所講的變數“被提前”是完全一致的,並且,由於“被提前”的變數的預設值是 undefined ,所以報的錯誤屬於“型別不匹配”,因為 undefined 不是函式,當然不能被呼叫。

總結 
通過上面的講解可以總結如下:

  • 變數的宣告被提前到作用域頂部,賦值保留在原地
  • 函式宣告整個“被提前”
  • 函式表示式時,只有變數“被提前”了,函式沒有“被提前”

3、var的副作用

隱式全域性變數和明確定義的全域性變數間有些小的差異,就是通過delete操作符讓變數未定義的能力。

  • 通過var建立的全域性變數(任何函式之外的程式中建立)是不能被刪除的。
  • 無var建立的隱式全域性變數(無視是否在函式中建立)是能被刪除的。

這表明,在技術上,隱式全域性變數並不是真正的全域性變數,但它們是全域性物件的屬性。屬性是可以通過delete操作符刪除的,而變數是不能的:

// 定義三個全域性變數
var global_var = 1;
global_novar = 2;       // 反面教材
(function () {
   global_fromfunc = 3; // 反面教材
}());

// 試圖刪除
delete global_var;      // false
delete global_novar;    // true
delete global_fromfunc; // true

// 測試該刪除
typeof global_var;      // "number"
typeof global_novar;    // "undefined"
typeof global_fromfunc; // "undefined"

在ES5嚴格模式下,未宣告的變數(如在前面的程式碼片段中的兩個反面教材)工作時會丟擲一個錯誤。

4、單var形式宣告變數

在函式頂部使用單var語句是比較有用的一種形式,其好處在於:

  • 提供了一個單一的地方去尋找功能所需要的所有區域性變數
  • 防止變數在定義之前使用的邏輯錯誤
  • 少程式碼(型別啊傳值啊單線完成) 
    單var形式長得就像下面這個樣子:
function func() {
   var a = 1,
       b = 2,
       sum = a + b,
       myobject = {},
       i,
       j;
   // function body...
}

您可以使用一個var語句宣告多個變數,並以逗號分隔。像這種初始化變數同時初始化值的做法是很好的。這樣子可以防止邏輯錯誤(所有未初始化但宣告的變數的初始值是undefined)和增加程式碼的可讀性。在你看到程式碼後,你可以根據初始化的值知道這些變數大致的用途。

相關文章