【譯】學習JavaScript中提升、作用域、閉包的終極指南

燎原之火發表於2019-02-28

這似乎令人驚訝,但在我看來,理解JavaScript語言最重要和最基本的概念是理解執行上下文。通過正確學習它,你將很好地學習更多高階主題,如提升,作用域鏈和閉包。考慮到這一點,究竟什麼是“執行上下文”?為了更好地理解它,我們首先來看看我們如何編寫軟體。

編寫軟體的一種策略是將程式碼分解為單獨的部分。雖然這些“部分”有許多不同的名稱(功能,模組,包等),但它們都是為了一個目的而存在 - 分解和管理應用程式的複雜性。現在,不要像編寫程式碼的人那樣思考,而是根據JavaScript工具來解釋程式碼。我們可以使用相同的策略,將程式碼分成幾部分,管理解釋程式碼的複雜性,就像我們編寫程式碼一樣嗎?事實證明是可以的,而這些“部分”被稱為執行上下文。就像函式/模組/包允許你管理編寫程式碼的複雜性一樣,執行上下文允許JavaScript引擎管理解釋和執行程式碼的複雜性。現在我們知道了執行上下文的目的,我們需要回答的下一個問題是它們是如何建立的以及它們是由什麼組成的?

JavaScript引擎執行程式碼時建立的第一個執行上下文稱為“全域性執行上下文”。最初這個執行上下文將包含兩個東西 - 全域性物件和一個被呼叫的變數this。this將引用全域性物件,如果在瀏覽器中執行JavaScript,全域性物件就是window物件,如果是在Node環境中執行,全域性物件就是global。

【譯】學習JavaScript中提升、作用域、閉包的終極指南

上面我們可以看到,即使沒有任何程式碼,全域性執行上下文仍將包含兩樣東西 - window和this。這是最基本形式的全域性執行上下文。 讓我們一步一步,看看當我們開始實際向程式中新增程式碼時會發生什麼。讓我們從新增一些變數開始。

【譯】學習JavaScript中提升、作用域、閉包的終極指南
【譯】學習JavaScript中提升、作用域、閉包的終極指南
你能發現上面兩張圖片之間的差異嗎?關鍵的一點是,每個執行環境都有兩個獨立的階段,一個建立階段(Creation)和一個執行階段(Execution),每個階段都有自己獨特的職責。

在全域性Creation階段,JavaScript引擎將

建立一個全域性物件。 建立一個名為“this”的物件。 為變數和函式設定記憶體空間。 在記憶體中放置任何函式宣告時,為變數宣告分配預設值“undefined”。 直到Execution階段JavaScript引擎開始逐行執行程式碼並執行。

我們可以在下面的GIF中看到這個流程從一個Creation階段到Execution另一個階段。

【譯】學習JavaScript中提升、作用域、閉包的終極指南

在Creation階段window和this建立過程中,變數宣告(name和handle)被賦值為預設值undefined,並且任何函式宣告(getUser)都完全放在記憶體中。然後,一旦我們進入Execution階段,JavaScript引擎就會逐行開始執行程式碼,並將實際值分配給已經存在於記憶體中的變數。

GIF很酷,但不像單步執行程式碼並親自檢視過程一樣酷。因為你應得的,我為你建立了JavaScript Visualizer。如果你想檢視上面的程式碼,請使用此連結

要真正理解Creation階段和Execution階段,讓我們輸出一些在Creation階段之後和Execution階段之前的值。

console.log('name: ', name)
console.log('handle: ', handle)
console.log('getUser :', getUser)

var name = 'Tyler'
var handle = '@tylermcginnis'

function getUser () {
  return {
    name: name,
    handle: handle
  }
}
複製程式碼

在上面的程式碼中,你希望將會有哪些內容輸出到控制檯?當JavaScript引擎逐行開始執行我們的程式碼並呼叫我們的console.logs時,Creation解析已經開始了。這意味著,正如我們之前看到的那樣,變數宣告應該被分配一個值undefined,而函式宣告應該已經完全在記憶體中了。正如我們所期望的那樣,name和handle的值都是是undefined,而getUser是對記憶體中函式的引用。

console.log('name: ', name) // name: undefined
console.log('handle: ', handle) // handle: undefined
console.log('getUser :', getUser) // getUser: ƒ getUser () {}

var name = 'Tyler'
var handle = '@tylermcginnis'

function getUser () {
  return {
    name: name,
    handle: handle
  }
}
複製程式碼

undefined在建立階段將變數宣告分配為預設值的過程稱為“提升”。

希望你有一個'啊哈!'的時刻。之前可能已經向你解釋過“提升”但沒有取得多大成功。“提升”令人困惑是因為沒有任何東西實際上是“提升”或移動的。既然你已經理解了執行上下文並且undefined在Creation階段中為變數宣告分配了預設值,那麼你就會理解“提升”,因為它實際上就是它的全部內容。

此時,你應該對全域性執行上下文及其兩個階段非常熟悉,Creation並且Execution。好訊息是,你只需要學習其他一個執行上下文,它與全域性執行上下文幾乎完全相同。它被稱為函式執行上下文,只要呼叫一個函式就會建立它。

這是關鍵。建立執行上下文的唯一時機是JavaScript引擎首次開始解釋程式碼(全域性執行上下文)以及每當呼叫函式時。

現在我們需要回答的主要問題是全域性執行上下文和函式執行上下文之間的區別。在之前如果你記得,我們說在全域性Creation階段,JavaScript引擎會

建立一個全域性物件。 建立一個名為“this”的物件。 為變數和函式設定記憶體空間。 在記憶體中放置任何函式宣告時,為變數宣告分配預設值“undefined”。 當我們談論函式執行上下文時,哪些步驟沒有意義?是第一步。在全域性執行上下文Creation階段建立時,我們應該只有一個全域性物件,而不是每次呼叫函式時建立並且JavaScript引擎建立一個函式執行上下文。與全域性執行上下文建立全域性物件相反,函式執行上下文只需關注引數物件。考慮到這一點,我們可以調整我們之前的列表。每當建立一個函式執行上下文時,JavaScript引擎都會

1.建立一個全域性物件。 (不同點,全域性上下文特性)
1.建立一個引數物件。
2.建立一個名為this的物件。
3.為變數和函式設定記憶體空間。
4.在記憶體中放置任何函式宣告時,為變數宣告分配預設值“undefined”。
複製程式碼

為了看到這一點,讓我們回到我們之前的程式碼,但這一次,而不僅僅是定義getUser,讓我們看看當我們呼叫它時會發生什麼。

檢視視覺化程式碼

【譯】學習JavaScript中提升、作用域、閉包的終極指南

正如我們所討論的那樣,當我們呼叫getUser時,新的執行上下文就會建立。在getUsers執行上下文Creation階段,JavaScript引擎建立一個this物件和一個arguments物件。因為getUser沒有任何變數,JavaScript引擎不需要設定任何記憶體空間或“提升”任何變數宣告。

你可能還注意到,當getUser函式執行完畢後,它將從視覺化中刪除。實際上,JavaScript引擎會建立所謂的“執行堆疊”(也稱為“呼叫堆疊”)。無論何時呼叫函式,都會建立一個新的執行上下文並將其新增到執行堆疊中。每當函式完成同時執行Creation和Execution階段時,它就會從執行堆疊中彈出。因為JavaScript是單執行緒的(意味著一次只能執行一個任務),所以這很容易視覺化。使用“JavaScript Visualizer”,執行堆疊以巢狀方式顯示,每個巢狀專案都是執行堆疊上的新執行上下文。

檢視視覺化程式碼

【譯】學習JavaScript中提升、作用域、閉包的終極指南

在這一點上,我們已經看到函式呼叫如何建立自己的執行上下文,這些執行上下文放在執行堆疊上。我們還沒有看到的是區域性變數如何發揮作用。讓我們更改程式碼,以便我們的函式具有區域性變數。

檢視視覺化程式碼

【譯】學習JavaScript中提升、作用域、閉包的終極指南

這裡沒有重要的細節需要注意。首先,你傳入的任何引數都將作為本地變數新增到該函式的執行上下文中。在該示例中handle,作為全域性執行上下文中的變數(因為它是定義它的位置)以及getURL執行上下文存在,因為我們將其作為引數傳遞。接下來是在函式內部宣告的變數存在於該函式的執行上下文中。因此,我們建立的時候twitterURL,它存活在內部getURL執行上下文,因為這就是它的定義,不是在全域性執行上下文。這似乎是顯而易見的,但它是我們下一個主題作用域的基礎。

在過去,你可能會聽到“作用域”的定義,即“變數可訪問的位置”。無論當時是否有意義,憑藉你對執行上下文和JavaScript Visualizer工具的新發現,作用域將比以往更加清晰。實際上,MDN將“作用域”定義為“當前執行的上下文。”聽起來很熟悉?我們可以以與我們如何考慮執行上下文非常相似的方式來思考“作用域”或“變數可訪問的位置”。

這是對你的測試。bar當它記錄在下面的程式碼中時會是什麼?

function foo () {
  var bar = 'Declared in foo'
}

foo()

console.log(bar)
複製程式碼

讓我們在JavaScript Visualizer中檢視它。

檢視視覺化程式碼

【譯】學習JavaScript中提升、作用域、閉包的終極指南

當foo呼叫我們建立的執行堆疊一個新的執行上下文。該Creation階段建立this,arguments並設定bar到undefined。然後Execution發生階段並將字串分配Declared in foo給bar。之後,Execution階段結束,foo執行上下文從堆疊中彈出。一旦foo從執行堆疊中刪除,我們就會嘗試登入bar控制檯。在那一刻,根據JavaScript Visualizer,它似乎bar從未存在過,所以我們得到了undefined。這向我們展示的是,在函式內部建立的變數是區域性作用域的。這意味著(大多數情況下,我們稍後會看到異常)一旦從執行堆疊中彈出了函式的執行上下文,就無法訪問它們。

這是另一個。程式碼執行完畢後將記錄到控制檯的內容是什麼?

function first () {
  var name = 'Jordyn'

  console.log(name)
}

function second () {
  var name = 'Jake'

  console.log(name)
}

console.log(name)
var name = 'Tyler'
first()
second()
console.log(name)
複製程式碼

再說一次,我們來看看JavaScript Visualizer。

檢視視覺化程式碼

【譯】學習JavaScript中提升、作用域、閉包的終極指南

我們得到undefined,Jordyn,Jake,然後Tyler。這告訴我們的是,你可以將每個新的執行上下文視為擁有自己獨特的可變環境。即使存在包含變數的其他執行上下文,JavaScript引擎也將首先檢視該變數的當前執行上下文。

這就提出了一個問題,如果變數在當前的執行上下文中不存在怎麼辦?JavaScript引擎會停止嘗試查詢該變數嗎?讓我們看一個能回答這個問題的例子。在下面的程式碼中,將記錄什麼?

var name = 'Tyler'

function logName () {
  console.log(name)
}

logName(
複製程式碼

檢視視覺化程式碼

【譯】學習JavaScript中提升、作用域、閉包的終極指南

你的直覺可能是它會輸出記錄,undefined因為logName執行上下文name在其範圍內沒有變數。這是公平的,但這是錯誤的。如果JavaScript引擎無法在函式的執行上下文中找到本地變數,它會查詢該變數的最近父執行上下文。此查詢鏈將一直持續到引擎到達全域性執行上下文。在這種情況下,如果全域性執行上下文沒有變數,它將丟擲一個引用錯誤。

如果本地執行上下文中不存在變數,則JavaScript引擎逐個進行並檢查每個單獨的父執行上下文的過程稱為作用域鏈。JavaScript Visualizer通過使每個新的執行上下文縮排並具有唯一的彩色背景來顯示範圍鏈。在視覺上,你可以看到任何子執行上下文都可以引用位於其任何父執行上下文中的任何變數,但反之亦然。

之前我們瞭解到,在函式內部建立的變數是本地作用域的,一旦函式的執行上下文從執行堆疊中彈出,它們就不能(大部分)被訪問。現在是時候深入研究“ 大部分 ”了。如果你有一個巢狀在另一個函式內的函式,那麼這種情況並非如此。在這種情況下,即使從執行堆疊中刪除了父函式的執行上下文,子函式仍然可以訪問外部函式的作用域。那是很多話。與往常一樣,JavaScript Visualizer可以幫助我們在這裡。

檢視視覺化程式碼

【譯】學習JavaScript中提升、作用域、閉包的終極指南

請注意,在makeAdder執行堆疊中彈出執行上下文後,JavaScript Visualizer會建立所謂的閉包作用域。其中閉包作用域包含makeAdder執行上下文中存在的相同變數環境。發生這種情況的原因是因為我們有一個巢狀在另一個函式內部的函式。在我們的示例中,inner函式巢狀在函式內部makeAdder,因此在變數環境中inner建立。即使在執行堆疊中彈出執行環境之後,由於建立了執行堆疊,因此可以訪問變數(通過作用域鏈)。ClosuremakeAddermakeAdderClosure Scopeinnerx

正如你可能猜到的那樣,呼叫子函式“訪問”其父函式的可變環境的概念稱為閉包。

福利貼

以下是一些我知道的相關話題,如果我沒有提及有人會打電話給我?。

全域性變數

在瀏覽器中,只要在全域性執行上下文中建立變數(在任何函式之外),該變數將作為屬性新增到window物件上。

在這兩種瀏覽器和節點,如果你建立一個沒有宣告的變數(即無var,let或const),該變數也將被新增為全域性物件上的屬性。

// In the browser
var name = 'Tyler'

function foo () {
  bar = 'Created in foo without declaration'
}

foo()

console.log(window.name) // Tyler
console.log(window.bar) // Created in foo without declaration
複製程式碼

let和const

視訊連結,需梯子

this關鍵字

在本文中,我們瞭解到,在Creation每個執行上下文階段,JavaScript引擎都會建立一個名為的物件this。如果你想進一步瞭解為什麼這麼重要以及如何確定this關鍵字是什麼,我建議你閱讀WTF是這樣的 - 理解這個關鍵字,呼叫,應用和繫結在JavaScript中

相關連結

後記

gif檔案太大,無法上傳,可以點選檢視視覺化程式碼除錯程式碼。 以上譯文僅用於學習交流,水平有限,難免有錯誤之處,敬請指正。

相關文章