前端進擊的巨人(一):執行上下文與執行棧,變數物件
寫在開篇
已經不敢自稱前端小白,曾經吹過的牛逼總要一點點去實現。
正如前領導說的,自己喝酒吹過的牛皮,跪著都得含著淚去實現。
那麼沒有年終完美總結,來個新年莽撞開始可好。
進擊巨人系列開篇,不忘初心,砥礪前行。
理解執行上下文
執行上下文(Execution Context): 函式執行前進行的準備工作(也稱執行上下文環境)
執行JavaScript程式碼時,當代碼執行進入一個環境時,就會為該環境建立一個執行上下文,它會在你執行程式碼前做一些準備工作,如確定作用域,建立區域性變數物件等。
具體做了什麼先按下不表,先來看下JavaScript執行環境有哪些?
JavaScript中執行環境
- 全域性環境
- 函式環境
- eval函式環境 (已不推薦使用)
那麼與之對應的執行上下文型別同樣有3種:
執行上下文的型別
- 全域性執行上下文
- 函式執行上下文
- eval函式執行上下文
JavaScript執行時首先會進入全域性環境,對應會生成全域性上下文。程式程式碼中基本都會存在函式,那麼呼叫函式,就會進入函式執行環境,對應就會生成該函式的執行上下文。
先插播一個知識點:"JS是單執行緒"! "單執行緒"! "單執行緒"!
簡單理解下單執行緒,就是同個時間段只能做一件任務,完成之後才可以繼續下一個任務。正如女朋友只有一個,各位面向物件的小夥伴們你們說對不對?有女票的必須說沒毛病。
既然是這樣,必須要有一個排隊機制,不然就會出現幾個流氓霸著車道不讓過,"還有王法麼?"
JS中管理多個執行上下文
函式程式設計中,程式碼中會宣告多個函式,對應的執行上下文也會存在多個。在JavaScript中,通過棧的存取方式來管理執行上下文,我們可稱其為執行棧,或函式呼叫棧(Call Stack)。
在說明執行棧前,先來補下"棧資料結構"知識點。
棧資料結構
藉助前端大神的例子,用乒乓球盒子來理解棧的存取方式。(這個例子讓我徹底記住了棧資料結構)
棧遵循"先進後出,後進先出"的規則,或稱LIFO ("Last In First Out") 規則。
如圖所示,我們只能從棧頂取出或放入乒乓球,最先放進盒子的總是最後才能取出。
棧中"放入/取出"
總結棧資料結構的特點:
- 後進先出,先進後出
- 出口在頂部,且僅有一個
執行棧(函式呼叫棧)
理解完棧的存取方式,我們接著分析JavaScript中如何通過棧來管理多個執行上下文。
程式執行進入一個執行環境時,它的執行上下文就會被建立,並被推入執行棧中(入棧);
程式執行完成時,它的執行上下文就會被銷燬,並從棧頂被推出(出棧),控制權交由下一個執行上下文。
因為JS執行中最先進入全域性環境,所以處於"棧底的永遠是全域性環境的執行上下文"。而處於"棧頂的是當前正在執行函式的執行上下文",當函式呼叫完成後,它就會從棧頂被推出(理想的情況下,閉包會阻止該操作,閉包後續文章深入詳解)。
"全域性環境只有一個,對應的全域性執行上下文也只有一個,只有當頁面被關閉之後它才會從執行棧中被推出,否則一直存在於棧底"
文字太多不如上程式碼系列 ——》程式碼 + 圖,一覽無遺:
function foo () {
function bar () {
return 'I am bar';
}
return bar();
}
foo();
執行上下文的生命週期
執行上下文的生命週期有兩個階段:
建立階段(進入執行上下文)
執行階段(程式碼執行)
建立階段:函式被呼叫時,進入函式環境,為其建立一個執行上下文,此時進入建立階段
執行階段:執行函式中程式碼時,此時執行上下文進入執行階段
建立階段的操作
- 建立變數物件
- 函式環境會初始化建立
Arguments
物件(並賦值) - 函式宣告(並賦值)
- 變數宣告,函式表示式宣告(未賦值)
- 函式環境會初始化建立
- 確定this指向(this由呼叫者確定)
- 確定作用域(詞法環境決定,哪裡宣告定義,就在哪裡確定)
執行階段的操作
- 變數物件賦值
- 變數賦值
- 函式表示式賦值
- 呼叫函式
- 順序執行其它程式碼
看到這裡,我們不經會問變數物件是什麼鬼,它與程式碼中常見的函式宣告,變數宣告有神馬關係???
變數物件和活動物件的區別:
當進入到一個執行上下文後,這個變數物件才會被啟用,所以叫活動物件(AO),這時候活動物件上的各種屬性才能被訪問。
"建立階段對函式宣告做賦值,變數及函式表示式僅做宣告,真正的賦值操作要等到執行上下文程式碼執行階段"。
程式碼例子1:變數提升
function foo() {
console.log(a); // 輸出undefined
var a = 'I am here'; // 賦值
}
foo();
// 實際執行過程
function foo() {
var a; // 變數宣告,var初始化undefined
console.log(a);
a = 'I am here'; // 變數重新賦值
}
程式碼例子2:函式宣告優先順序
function foo() {
console.log(bar);
var bar = 20;
function bar() {
return 10;
}
var bar = function() {
return 30;
}
}
foo(); // 輸出bar()整個函式宣告
函式宣告,變數宣告,函式表示式的優先順序
- 函式宣告,如果有同名屬性,會替換掉
- 變數,函式表示式
- 函式宣告優先 > 變數,函式表示式
執行上下文的數量限制(堆疊溢位)
執行上下文可存在多個,雖然沒有明確的數量限制,但如果超出棧分配的空間,會造成堆疊溢位。常見於遞迴呼叫,沒有終止條件造成死迴圈的場景。
// 遞迴呼叫自身
function foo() {
foo();
}
foo();
// 報錯: Uncaught RangeError: Maximum call stack size exceeded
文末總結
- JavaScript是單執行緒
- 棧頂的執行上下文處於執行中,其它需要排隊
- 全域性上下文只有一個處於棧底,頁面關閉時出棧
- 函式執行上下文可存在多個,但應避免遞迴時堆疊溢位
- 函式呼叫時就會建立新的上下文,即使呼叫自身,也會建立不同的執行上下文
參考文件:
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。