執行上下文、做用域鏈和JS內部機制(Execution context, Scope chain and JavaScript internals)javascript
執行上下文(Execution context EC)是js代碼的執行環境,它包括 this的值、變量、對象和函數。
全局上下文是文件第一次加載到瀏覽器,js代碼開始執行的默認執行上下文。在瀏覽器環境中,嚴格模式下this的值爲undefined,不然this的值爲window對象。GEC只能有一個(由於js執行的全局環境只能有一個)。java
函數執行時建立函數執行上下文,每一個函數都有本身的執行上下文。FEC能夠獲取到GEC中的內容。當在全局上下文中執行代碼時js引擎發現一個函數調用,則建立一個函數執行上下文。瀏覽器
執行eval時建立ide
執行上下文棧Execution context stack (ECS)是執行js代碼時建立的執行棧結構。 GEC默認在棧的最裏層,當js引擎發現一個函數調用,則建立這個函數的 FEC並push進棧,js引擎執行棧頂上下文關聯的函數,一旦函數執行完,則將其 FEC pop出棧,並往下執行。
看個例子(動圖插不了棧動圖連接)函數
var a = 10; function functionA() { console.log("Start function A"); function functionB(){ console.log("In function B"); } functionB(); } functionA(); console.log("GlobalContext");
上面討論了js引擎如何處理執行上下文(push和pop),下面討論js引擎如何建立執行上下文,這個過程分爲兩個階段:建立階段和執行階段。this
js引擎調用函數,但函數還沒開始執行階段。
js引擎在這個階段對整個函數進行一個編譯(compile the code),主要乾了下面三件事:code
可變對象是包含全部變量、函數參數和內部函數聲明信息的特殊對象,它是一個特殊對象且沒有__proto__屬性。cdn
一旦可變對象建立完,js引擎就開始初始化做用域鏈。做用域鏈是一個當前函數所在的可變對象的列表,其中包括GEC的可變對象和當前函數的可變對象。對象
初始化this的值ip
下面經過一個例子進行說明
function funA (a, b) { var c = 3; var d = 2; d = function() { return a - b; } } funA(3, 2);
當調用funA和執行funA前的這段時間,js引擎爲funA建立了一個executionContextObj以下
executionContextObj = { variableObject: {}, // All the variable, arguments and inner function details of the funA scopechain: [], // List of all the scopes inside which the current function is this // Value of this }
可變對象包含參數對象(包含函數參數的細節),聲明的變量和函數,以下所示
variableObject = { argumentObject : { 0: a, 1: b, length: 2 }, a: 3, b: 2 c: undefined, d: undefined then pointer to the function defintion of d }
在此階段,js引擎會重掃一遍函數,用具體的變量的值來更新 可變對象,並執行代碼內容。
執行階段執行完後,可變對象的值以下:
variableObject = { argumentObject : { 0: a, 1: b, length: 2 }, a: 3, b: 2, c: 3, d: undefined then pointer to the function defintion of d }
代碼以下
a = 1; var b = 2; cFunc = function(e) { var c = 10; var d = 15; a = 3 function dFunc() { var f = 5; } dFunc(); } cFunc(10);
當瀏覽器加載上面的代碼後,js引擎進入編譯階段,只處理聲明,不處理值。下面走讀一遍代碼:
此時的
globalExecutionContextObj = { variableObject: { // 原文中有時用activationObj argumentObj : { length:0 }, b: undefined, cFunc: Pointer to the function definition }, scopeChain: [GLobal execution context variable object], this: value of this }
再接着上面,js引擎進入執行階段並再過一遍。此時將會更新變量名和執行
此時
globalExecutionContextObj = { variableObject: { argumentObj : { length:0 }, b: 2, cFunc: Pointer to the function definition, a: 1 }, scopeChain: [GLobal execution context variable object], this: value of this }
因爲cFunc有個參數e,js引擎會在cFunc執行上下文對象可變對象添加e屬性,並初始化爲2
此時
cFuncExecutionContextObj = { activationbj: { argumentObj : { 0: e, length:1 }, e: 10, c: undefined, d: undefined dFunc: Pointer to the function definition, }, scopeChain: [cFunc variable object, Global exection context variable object], this: value of this }
cFuncExecutionContextObj = { activationbj: { argumentObj : { 0: e, length:1 }, e: 10, c: 10, d: 15 dFunc: Pointer to the function definition, }, scopeChain: [cFunc variable object, Global exection context variable object], this: value of this }
調用dFunc,js引擎再次進入編譯階段,建立dFunc執行上下文對象。
dFunc執行上下文對象能夠訪問到cFunc和全局做用域中的全部變量和函數;一樣cFunc能夠訪問到全局的,但不能訪問dFunc中的;全局上下文對象不能訪問cFunc和dFunc中的變量和對象。
有了上面的概念,對hoisting(變量提高)應該更容易理解了。
做用域鏈是當前函數所在的可變對象列表
看下面一段代碼
a = 1; var b = 2; cFunc = function(e) { var c = 10; var d = 15; console.log(c); console.log(a); function dFunc() { var f = 5; console.log(f) console.log(c); console.log(a); } dFunc(); } cFunc(10);
當cFunc被調用時,cFunc的做用域鏈以下
Scope chain of cFunc = [ cFunc variable object, Global Execution Context variable object]
當dFunc被調用時,dFunc在cFunc中,dFunc的做用域鏈包含dFunc、cFunc和全局可變對象
Scope chain of dFunc = [dFunc variable object, cFunc variable object, Global execution context variable object]
當咱們嘗試訪問dFunc中的f,js引擎查看f是否可從dFunc的可變對象中獲取,找到console輸出;
訪問c變量,js引擎首先在dFunc的可變對象中獲取,不能獲取,則到cFunc的可變對象中去獲取,找到console輸出;
訪問a變量,同上,最後找到GEC的可變對象,獲取到並console輸出
一樣,cFunc中獲取c和a相似
在cFunc中訪問不到f變量,但dFunc中能夠經過做用域鏈獲取到c和d