大話javascript 2期:執行上下文與執行上下文棧
一、什麼是執行上下文?
執行上下文(Execution Context): 函式執行前進行的準備工作(也稱執行上下文環境)
JavaScript在執行一個“程式碼段”之前,即解析(預處理)階段,會先進行一些“準備工作”,例如掃描JS中var定義的變數、函式名等,進而生成執行上下文。
name | - |
---|---|
變數物件(VO, variable object) | 當前函式定義的變數、函式、引數 |
作用域鏈(Scope chain) | 原始碼定義時形成的作用域鏈 |
this |
JS中的“程式碼段”分為三種:全域性程式碼段、函式體程式碼段、eval程式碼段。(注:ES6之前,JS不存在“程式碼塊”作用域的概念,即除了函式之外所有“{}”裡的程式碼,都屬於全域性作用域)
全域性程式碼段“準備工作”包括:
1.變數、函式表示式 —— 變數宣告,預設賦值為undefined; 2.this —— 賦值; 3.函式宣告 —— 賦值。
函式體程式碼段“準備工作”包括:
1.變數、函式表示式 —— 變數宣告,預設賦值為undefined; 2.this —— 賦值; 3.函式宣告 —— 賦值; 4.引數 —— 賦值; 5.argument —— 賦值; 6.自由變數的取值作用域 —— 賦值。
evel()不推薦使用,所以不再分析evel程式碼段。
至此,“執行上下文”的定義可以通俗化為 —— 在執行程式碼段之前(預處理階段),把將要用到的所有變數都事先拿出來,有的直接賦值,有的先用undefined佔個空,這些變數共同組成的詞法環境,即為執行上下文環境
。
二、執行上下文棧
javaScript是單執行緒語言,簡單理解下單執行緒,就是同個時間段只能做一件任務,完成之後才可以繼續下一個任務。
函式程式設計中,程式碼中會宣告多個函式,對應的執行上下文也會存在多個。在JavaScript中,通過棧的存取方式來管理執行上下文,我們可稱其為執行棧,或函式呼叫棧(Call Stack)。
1.棧資料結構
要簡單理解棧的存取方式,我們可以通過類比乒乓球盒子來分析。如下圖左側。
棧遵循" 先進後出,後進先出
"的規則,或稱LIFO ("Last In First Out") 規則。
如圖所示,我們只能從棧頂取出或放入乒乓球,最先放進盒子的總是最後才能取出。
棧中"放入/取出",也可稱為"入棧/出棧"。
總結棧資料結構的特點:
- 後進先出,先進後出
- 出口在頂部,且僅有一個
2.執行上下文棧ECS(Execution Context Stack)(函式呼叫棧)
程式執行進入一個執行環境時,它的執行上下文就會被建立,並被推入執行棧中(入棧);
因為JS執行中最先進入全域性環境,所以處於" 棧底的永遠是全域性環境的執行上下文
"。而處於" 棧頂的是當前正在執行函式的執行上下文
",當函式呼叫完成後,它就會從棧頂被推出(理想的情況下,閉包會阻止該操作,閉包後續文章深入詳解)。
"全域性環境只有一個,對應的全域性執行上下文也只有一個,只有當頁面被關閉之後它才會從執行棧中被推出,否則一直存在於棧底"
function foo () { function bar () { return 'I am bar'; } return bar(); } foo();
3.執行上下文的生命週期
執行上下文的生命週期有兩個階段:
- 建立階段(進入執行上下文)
- 執行階段(程式碼執行)
建立階段:函式被呼叫時,進入函式環境,為其建立一個執行上下文,此時進入建立階段
執行階段:執行函式中程式碼時,此時執行上下文進入執行階段
建立階段的操作
-
建立變數物件
- 函式環境會初始化建立Arguments物件(並賦值)
- 函式宣告(並賦值)
- 變數宣告,函式表示式宣告(未賦值)
- 確定this指向(this由呼叫者確定)
- 確定作用域(詞法環境決定,哪裡宣告定義,就在哪裡確定)
執行階段的操作
-
變數物件賦值
- 變數賦值
- 函式表示式賦值
2.呼叫函式
3.順序執行其它程式碼
變數物件和活動物件的區別:
當進入到一個執行上下文後,這個變數物件才會被啟用,所以叫活動物件(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()整個函式宣告
函式宣告,變數宣告,函式表示式的優先順序
- 函式宣告,如果有同名屬性,會替換掉
- 變數,函式表示式
- 函式宣告優先 > 變數,函式表示式
4.執行上下文的數量限制(堆疊溢位)
執行上下文可存在多個,雖然沒有明確的數量限制,但如果超出棧分配的空間,會造成堆疊溢位。常見於遞迴呼叫,沒有終止條件造成死迴圈的場景。
// 遞迴呼叫自身 function foo() { foo(); } foo(); // 報錯: Uncaught RangeError: Maximum call stack size exceeded
三、執行上下文流程圖