我相信不少前端初學者一開始都會被執行上下文這個概念弄暈,或者說似懂非懂。對於工做兩年的我來講,說來實在慚愧,雖然知道它大概是什麼,但總以爲沒有一個更爲清晰的認識(沒法把它的工做過程描述清楚),所以最近特地溫習了一遍,寫下了這篇文章前端
要說清它的大致工做流程,須要提早說明三個基本概念,分別是thread of exection(線程)
、variable envirnoment(變量環境)
、call Stack(調用棧)
,這些概念咱們或多或少接觸過,接下來我會經過一段示例代碼,和一系列圖片,進一步解釋這三個概念在執行上下文的運做流程。編程
const num = 2;
function addOne(input) {
const output = input + 1;
return output;
}
const result = addOne(2);
複製代碼
在運行上面這些代碼前,js 引擎作的第一件是就是建立一個global execution context
,也就是全局執行上下文:數據結構
thread
的執行順序,衆所周知 js 是單線程的,它會一行行、從上往下去執行代碼;而右邊的
global memory
,它用於存儲當前上下文中的數據,因爲線程目前處於全局上下文環境,故加了個
global
的前綴。
在這段代碼中,第一行咱們聲明瞭一個名爲num
的不可變變量,並賦值爲4
, 所以global memory
中就會分配內存,存儲這個變量:閉包
addOne
的變量,並把一個函數賦值於它,那在
global memory
裏,到底存的是個啥?爲了解答這個問題,我特地打印了一下:
function addOne(input) {
const output = input + 1;
return output;
}
console.log(addOne);
複製代碼
其實很容易理解,當執行第二行的時候,該函數並無被調用,所以線程不會馬上解析裏面的內容,而是把它內部的信息以「文本內容」的形式保存下來,當須要執行的時候,纔去解析變量裏的函數內容,這也很好地解析了爲何函數內的異常僅會在函數被調用時才拋出來。函數式編程
所以這時global execution context
長這樣:函數
addOne
裏保存的是函數內容,目前對於線程而言它是未知的,所以咱們這裏特地用一個帶有輸入輸出箭頭的函數圖標,表明它是一個未被解析的函數。
咱們繼續執行第三步:仍是建立了一個變量result
,但此時它被賦予undefined
,由於線程暫時沒法從addOne
這個函數裏獲知它的返回值。post
addOne
函數被調用了,線程會從剛剛保存的
addOne
變量中取出內容,去解析、執行它。這時 js 就建立了一個新的執行上下文——
local execution context
,即當前的執行上下文,這是一個船新的上下文,所以我特地用一個新圖片去描述它:
memory
我加了個
local
前綴,表面當前存儲的都是此上下文中的變量數據。不管是上述的
global memory
,亦或是如今、或將來的
local memory
,咱們能夠用一個更爲專業的術語
variable envirnoment
去描述這種存儲環境。
此外,這個黑箭頭我特地畫了個拐角,意味它是從全局上下文進來的,local memory
首先會分配內存給變量input
,它在調用時就被2
賦值了,緊接着又建立了一個output
標籤並把計算結果3
賦值給它。最後,當線程遇到離開上下文的標識——return
,便離開上下文,並把ouput
的結果一併返回出去。ui
此時,這個上下文就沒用了(被執行完了),因而乎垃圾回收便盯上了它,選擇一個恰當的時機把裏面的local memory
刪光光。spa
這時候有同窗會問道:你這個只是普通場景,那閉包怎麼解釋呢?線程
其實閉包是個比較大的話題,這裏也能夠簡單描述下:當return
的是一個函數的話,它返回的不只是函數自己,還會把local memory
中被引用的變量做爲此函數的附加屬性一併返回出去,這就比如印魚喜歡吸附在鯊魚身上通常,鱔魚不管去哪都帶着它,所以,不管這個函數在哪裏被調用,它都能在它自己附帶的local memory
中找到那個變量。若是你把返回的函數console.log
出來,也是可以找到它的,這裏就不詳說,關於閉包的更多概念(包括在函數式編程中的使用),有興趣的童鞋能夠看看這篇文章:Partial & Curry - 函數式編程
go on,回到了global execution context
,result
再也不孤零零地undefined
,而是拿到了可愛的3
:
到這裏,咱們的線程就完成了全部工做,能夠歇息了,等等.....好像漏了什麼沒講.....對,就是Call Stack
!
剛剛咱們全篇在講解thread of exection
多麼努力地一行行解析執行代碼,variable envirnoment
多麼勤快地存儲變量,那call stack
幹了什麼?是在偷懶嗎?
其實並非,call stack
起到了很是關鍵的做用:有了它,線程隨時能夠知道本身目前處於哪一個上下文。
試想一下,咱們在寫代碼的過程當中每每喜歡在各類函數內穿插着各類子函數,好比遞歸,所以勤勞的線程就得不斷地進入上下文、退出上下文、再進入、再進入、再退出、再進入,長此以往線程根本就不知道本身處於哪一個上下文中,也不知道應該在哪一個memory
中取數據,全都亂套了,所以必須經過一種方式去跟蹤、記錄目前線程所處的環境。
而call stack
就是一種數據結構,用於「存儲」上下文,經過不斷推入push
、推出pop
上下文的方式,跟蹤線程目前所處的環境——線程無需刻意記住本身身處何方,只要永遠處於最頂層執行上下文,就是當前函數執行的正確位置
程序一開始運行的時候,call stack
先會push
個global execution context
:
addOne
,
call stack
馬上將
addOne
的上下文
push
進去,待執行到
return
標識後,再
pop
出來:
push
、
pop
以及時刻處於最頂層上下文的原則,線程就能夠一直保持在正確的位置上:
call stack
層數是有上限的,所以稍加不注意,你寫的遞歸可能會形成
棧溢出
了。
簡單來講,上下文就是個能夠用於執行代碼的環境,與它相關的有三個重要的概念: