靈魂拷問,你真的懂 JavaScript 中的變量提升嗎?

引言

對於變量提升這個問題,我想從事前端的同學都或多或少認爲我懂這個。曾經,我也是這樣認爲的,我懂變量提升,並且可以從變量在 Chrome 中的內存分配講起,以及中間發生了什麼。

但是,在一次面試中,我遇到了幾個一起面前端的同學(當然技術水平參差不齊,並不是很高),在和他們聊這次筆試中的變量提升的問題時,發現大家都支支吾吾的,很多講的都是值的覆蓋

當時的面試題是這樣的:

function fn(a) {
    console.log(a)
    var a = 2
    function a() {}
    console.log(a)
}
fn(1)

這個題目,最終會輸出function a(){}2。那麼,爲什麼是這個答案,這個過程發生了什麼很重要。所以,今天我們就來徹底刨析一下變量提升的過程。

一、變量在內存中的分配

在分析整個過程前,我們先來回顧一下 JavaScript 中變量在內存中的分配。

大家都知道的是,對於原始類型會存儲在棧空間中,對於引用類型會將引用存儲在棧空間中,將數據存儲在堆空間中

其實這個過程還牽扯到函數上下文的創建,而每一個函數上下文中又會創建一個變量環境、詞法環境。有興趣的同學可以去看李斌老師的瀏覽器工作原理與實踐

所以,我們來看一個簡單的栗子,分析一下它在內存中的分配:
栗子:

var a = 1
var b = 2
var student = {name: 'wjc', age: 22}

它內存中的分配:

二、運行前的簡單編譯

衆所周知,JavaScript 是一門動態類型的語言,即它是在運行時確定變量的類型,不同於靜態類型語言的先編譯再運行的過程。但是,事實是 V8 引擎在解析運行 JavaScript 之前是會進行一次簡單的編譯,也就是我們通常所說的初始化過程。

這個初始化過程,會做這幾件事:

  • 區分執行代碼和變量聲明代碼
  • 變量聲明代碼劃分爲賦值代碼和初始化代碼
  • 初始化代碼有兩種情況,一是對變量(原生類型、對象類型)初始化爲 undefined;二是對函數的初始化,即直接指向函數在堆空間中的內存

那麼,我們就來看一個簡單的栗子:

console.log(a)
sayHi()
var a = 2
function sayHi() {
    console.log('Hi')
}

那麼按照我們上面所說,這段代碼的賦值只有 var a = 2,函數聲明只有進行編譯階段的代碼會是這樣的:

// 編譯代碼
var a = undefined
var sayHi = function () {
    console.log('Hi')
}

此時,它在內存中的分佈:

然後,在執行階段的代碼會是這樣:

// 執行代碼
console.log(a)
sayHi()
a = 2

所以,也就是當我們真正執行的時候會走執行代碼,所以很顯然會輸出:

undefined
Hi

而當走完所有執行代碼後,此時內存是這樣的:

我想通過這個栗子,大家應該大致搞懂變量提升的過程。但是,仍然存在一個較爲特殊的情況,就是當函數形參存在時的變量提升,也就是我們文章開頭提及的面試題。

三、函數形參的編譯執行

首先,我們需要對函數調用做一個簡單的理解,在我們平常調用函數的時候,真正會經歷兩個步驟

  • 如果此時存在形參,則進行函數形參的編譯和執行過程
  • 然後進入函數體,進行函數體內部的編譯和執行

可以看到這裏我們提到了當函數存在形參時,會先進行函數形參的編譯和執行過程。

這裏我們就來分析文章開頭這個栗子:

function fn(a) {
    console.log(a)
    var a = 2
    function a() {}
    console.log(a)
}
fn(1)

首先,此時是存在函數形參的,那麼函數形參的編譯和執行會是這樣:

var a = undefined
a = 1

然後,纔會進行函數體的編譯和執行:

// 編譯
a = function a() {} // 重點!!!
// 執行
console.log(a)
a = 2
console.log(a)

可以看到的是,如果函數體內的變量名和形參的變量名重複時,則不會進行普通變量的編譯賦值 undefined 的過程。但是,如果存在該變量是函數時,那麼則會進行函數變量的編譯賦值,即直接指向函數在堆空間中的地址。

所以,我們這個栗子在編譯後,可以看作是這樣的:

function fn() {
    var a = undefined
    a = 1
    a = function a() {}
    console.log(a)
    a = 2
    console.log(2)
}

很顯然,它會輸出會輸出function a(){}2

寫在最後

不知大家在深度理解過變量提升過程後,是否有和我一樣的感受就是學習編程的本質是追溯本源。現今,雖然我們可以用 ES6letconst 來聲明變量來避免 var 的種種缺陷。但是,如果因爲這樣而不去思考 var 爲什麼會存在這些缺陷。我想這是非常遺憾的。

寫作不易,如果你覺得有收穫的話,可以帥氣三連擊!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章