1. 程式人生 > >從ECMAScript規範深度分析JavaScript(一):執行期上下文

從ECMAScript規範深度分析JavaScript(一):執行期上下文

本文譯自Dmitry Soshnikov的《ECMA-262-3 in detail》系列教程。其中會加入一些個人見解以及配圖舉例等等,來幫助讀者更好的理解JavaScript。

前言

談到javascript不得不說執行期上下文——Execution context,執行期上下文是學習javascript必須要理解的一項內容,我們在這個系列的開始將首先來理解ECMAScript的執行期上下文以及它和可執行程式碼的區分。

執行期上下文

我們知道程式碼執行需要有控制權,每次當控制器轉到ECMAScript可執行程式碼的時候,就會進入到一個執行上下文,其中Dmitry Soshnikov提到“可執行程式碼的概念與抽象的執行上下文的概念是相對的。在某些時刻,可執行程式碼與執行上下文是等價的。”,我不認同這句話,可執行程式碼就是程式碼,永遠不可能和執行上下文等價,我們很容易從語言層面上將這兩個概念區分開來。

ECMAScript使用Execution context(EC)就是為了和可執行程式碼(executable code)的概念區分開來。剛才我們也講到了ECMAScript只是一個標準,他並沒有從技術實現的角度去規定EC的具體結構和型別,這是ECMAScript引擎實現ECMA標準時需要考慮的問題。

從邏輯上講,活動的執行期上下文會形成一個先進後出的棧結構,這個棧的底部總是全域性上下文(global context),棧頂部是當前執行期上下文。棧在各個執行期上下文進出棧的情況下被修改。

理解執行期上下文棧

這裡我們效仿Dmitry Soshnikov,可以定義一個數組來模擬執行上下文堆疊:
虛擬碼示例:

ECStack = [];

每次進入函式 (即使函式被遞迴或作為建構函式呼叫) 的時候或者內建的eval函式工作的時候,這個堆疊都會被推入。我們接下來詳解各種情況:

1、全域性程式碼

這類程式碼在程式級別處理,比如載入外部的js檔案或者本地的標籤中的內部程式碼,全域性程式碼不包括任何函式體內的部分。
在初始化(也就是程式開始)時,ECStack是這個樣子的:

ECStack = [
	globalContext
];

2、函式程式碼

當進入函式程式碼(所有型別的函式)時,ECStack會被推入新元素。要注意的是,具體的函式程式碼不包括內部函式(inner functions)程式碼。
如下所示,我們使函式自己調自己的方式遞迴一次:

(function foo(flag) {
  if (flag) {
    return;
  }
  foo(true);
})(false);

然後,ECStack的變化如下(ES6的情況有所不同,涉及尾呼叫的概念,我們本系列不涉及ES6的範圍):

// 第一次foo執行, ECStack.push(<foo> functionContext)
ECStack = [
  <foo> functionContext
  globalContext
];
  
// 遞迴呼叫foo時,ECStack.push(<foo> functionContext – recursively)
ECStack = [
  <foo> functionContext – recursively 
  <foo> functionContext
  globalContext
];

函式的每個返回都退出當前執行上下文,ECStack相應地彈出——連續地、倒過來地彈出——這是堆疊的很自然的實現。完成此程式碼的工作後,ECStack再次只包含globalContext——直到程式結束,ECStack變化如下:

// 遞迴呼叫返回時,ECStack.pop()
ECStack = [
  <foo> functionContext
  globalContext
];
// 立即執行函式返回時,ECStack.pop()
ECStack = [
  globalContext
];

其中要注意,丟擲但未捕獲的異常也可以退出一個或多個執行上下文:

(function foo() {
  (function bar() {
    throw 'Exit from bar and foo contexts';
  })();
})();

具體的ECStack的狀態和上面一樣,我就不再贅述一遍了。

3、eval程式碼

eval程式碼有些比較有意思的東西,在這裡有一個呼叫上下文(calling context)的概念——呼叫eval代函式的上下文。
eval的行為會影響呼叫上下文,比如變數或者函式的定義:

// 這裡會影響全域性上下文
eval('var x = 10');
 
(function foo() {
  // 這裡,變數y是foo函式區域性建立的變數
  // 影響的是foo函式的上下文
  eval('var y = 20');
})();
 
console.log(x); // 10
console.log(y); // "y" is not defined

相應的ECStack的變化:

ECStack = [
  globalContext
];
  
// eval('var x = 10');
ECStack.push({
  context: evalContext,
  callingContext: globalContext
});
 
// eval返回
ECStack.pop();
 
// 呼叫foo函式
ECStack.push(<foo> functionContext);
 
// eval('var y = 20');
ECStack.push({
  context: evalContext,
  callingContext: <foo> functionContext
});
 
// eval呼叫返回
ECStack.pop();
 
// foo函式返回
ECStack.pop();

你會發現這是一個很普通的邏輯棧呼叫。

注意:在ES5的嚴格模式(strict-code)中,eval已經不再影響呼叫它的上下文了,而是在區域性沙箱( local sandbox)中計算執行程式碼,在這裡我們對此先不作討論。

這裡有個更有趣的事情(大家不需要關注這個點,已經作為bug被修復了):
在老版本(1.7.0以下)的SpiderMonkey(Firefox的引擎)的實現中,可以把呼叫上下文作為第二個引數傳遞給eval。那麼,如果這個上下文存在,就有可能影響“私有”變數。

function foo() {
  var x = 1;
  return function () { alert(x); };
};
var bar = foo();
 
bar(); // 1
eval('x = 2', bar); // pass context, influence internal var "x"
bar(); // 2

然而,由於安全性問題,這在現代的引擎中已經被修復,不再具有意義了。

總結

這裡的理論是對將來對執行期上下文做詳細分析的基礎,比如變數物件(variable object)和作用域鏈,所以希望大家能深入瞭解這裡的內容。

希望此文能夠解決大家工作和學習中的一些疑問,避免不必要的時間浪費,有不嚴謹的地方,也請大家批評指正,共同進步!
轉載請註明出處,謝謝!

交流方式:QQ1670765991