《前端實戰總結》之變數提升,函式宣告提升及變數作用域詳解

徐小夕發表於2019-11-02

之所以會寫這篇文章,主要源於筆者在重構老專案的時候發現了一個bug,導致某個外掛不生效了,在review加search code加斷點除錯之後,發現了原因:一個同名的變數將外掛方法給覆蓋了,ohmyGad。

正文

1.變數是如何被覆蓋的

在一般情況下,js程式碼都是自上而下執行的,對於同一個變數,我們可以通過如下方式來修改:

var a = 1;
a = 2;
console.log(a)   // 2
a = function(){};
console.log(a)   // function(){};
複製程式碼

2.變數提升

上面的覆蓋過程大家都很好理解,那麼看如下的操作呢?

console.log(a);
var a = 1;
console.log(b);
var b = function(){};
複製程式碼

這個時候console.log()都會輸出undefined而不會報錯,這是為什麼呢?這裡就是變數提升起到的作用。我們在用var或者函式宣告的方式定義一個變數時,這個變數的定義會提升到方法體的最頂端,即如下所示:

var a = undefined;
var b = undefined;
console.log(a)
// ..
console.log(b)
複製程式碼

因此我們得出一條結論:

函式宣告和變數宣告總是會被直譯器悄悄地被"提升"到方法體的最頂部。

值得注意的是,我們使用let,const定義變數的時候,並不會發生提升,因為它存在區域性(塊)作用域的概念,會出現暫時性死區,所以在它們之前列印變數將報錯。如果對暫時性死區或者對es6不太瞭解的朋友可以參考我的另一篇文章,

快速掌握es6+新特性及es6核心語法盤點;

對let和const以及es6的新特性有詳細的介紹。

3.更近一步——變數提升的優先順序

直接剖出問題:

var a = 1;
function a(){
    console.log(a)
}
console.log(a)
複製程式碼

此時程式碼會列印什麼呢?答案是會列印1。這個問題也是我之前面試一些求職者的過程中錯誤高發區,這裡隱藏著一個概念:函式宣告提升的優先順序高於變數宣告的提升。瀏覽器底層的實現過程是這樣的:當js解析器在遇到函式宣告時,會優先將其提升到定義體頂部,其次再是var宣告的變數,這樣就導致函式a被變數a給覆蓋的情況,所以最終將列印1。

4.函式引數作用域與作用域鏈

作用域就是變數和函式的可訪問範圍,當程式碼在一個環境中執行時,會建立變數物件的一個作用域鏈(scope chain),來保證對執行環境有權訪問的變數和函式的順序訪問。作用域第一個物件始終是當前執行程式碼所在環境的變數物件。然後會一層層向外查詢,直到發現第一個指定的變數為止。

在瞭解完以上概念之後,我們來看看下面這個問題:

var a = {name: 'xuxi'};
function b(a){
    a.age = 12;
    a = {num: 1};
    return a
}
var a1 = b(a);
console.log(a, a1)
複製程式碼

上面程式碼列印的是什麼呢?其實這個是我今天出的面試題,還是因為一個朋友之前問了我這個問題,我覺得有必要總結一下。雖然今天的候選人沒有答出來,但是我相信在給他解釋完之後他應該不虛此行(說過了,不好意思)。

這塊主要還是函式內部作用域和引用型別的一個問題。具體過程如下:

(1)我們根據之前介紹的作用域和作用域鏈的概念可以知道,在函式體內,變數會就近查詢,而函式引數會存在於函式體內部作用域中,所以當我們把全域性變數a當作入參傳遞給函式時,又由於全域性a是引用型別,此時只是引用了它的地址,那麼我們通過a.age設定屬性時,全域性a也會改變。 (2)第二步是將a賦予了一個新的值,此時的a根據就近查詢其實是引數a,本質上是將引數a賦予了一個新的物件,這個時候和全域性變數的a沒有任何關係了,此時函式最後會返回一個新的物件。

綜上兩步分析,我們就會明白為什麼列印a時輸出的是{name: 'xuxi', age: 12},列印a1會輸出{num: 1}了。

總結

函式宣告提升,變數作用域以及作用域鏈這塊一直是學習javascript的基礎也是重點,所以希望這篇文章可以讓大家更好的掌握它。 如果想了解更多webpack,node,gulp,css3,javascript,nodeJS,canvas等前端知識和實戰,歡迎在公眾號《趣談前端》加入我們一起學習討論,共同探索前端的邊界。

《前端實戰總結》之變數提升,函式宣告提升及變數作用域詳解

更多推薦

相關文章