執行上下文 能夠理解爲當前代碼的執行環境,同一個函數在不一樣的環境中執行,會由於訪問數據的不一樣產生不同的結果。
執行上下文分爲三種:前端
window
對象),使this
指向這個全局對象eval
函數中的代碼時建立的執行上下文,少用且不建議使用執行上下文棧(Execution context stack,ECS),也叫函數調用棧(call stack
),是一種擁有 LIFO
(後進先出)數據結構的棧,用於存儲代碼執行時建立的執行上下文數組
因爲JS是單線程的,每次只能作一件事情,經過這種機制,咱們可以追蹤到哪一個函數正在執行,其餘函數在調用棧中排隊等待執行。瀏覽器
JS引擎第一次執行腳本時,會建立一個全局執行上下文壓到棧頂,而後隨着每次函數的調用都會建立一個新的執行上下文放入到棧頂中,隨着函數執行完畢後被執行上下文棧頂彈出,直到回到全局的執行上下文中。數據結構
代碼實例閉包
var color = 'blue'; function changeColor() { var anotherColor = 'red'; function swapColors() { var tempColor = anotherColor; anotherColor = color; color = tempColor; } swapColors(); } changeColor(); console.log(color); // red
執行過程能夠在 devTool
的 call stack
中看到,其中 anonyomus
爲全局上下文棧;其他爲函數上下文棧app
圖解: 函數
執行過程:this
全局執行上下文
,壓入執行棧,其中的可執行代碼開始執行。changeColor
函數,JS引擎中止執行全局執行上下文,激活函數 changeColor
建立它本身的執行上下文,且把該函數上下文放入執行上下文棧頂,其中的可執行代碼開始執行。changeColor
調用了 swapColors
函數,此時暫停了 changeColor
的執行上下文,建立了 swapColors
函數的新執行上下文,且把該函數執行上下文放入執行上下文棧頂。swapColors
函數執行完後,其執行上下文從棧頂出棧,回到了 changeColor
執行上下文中繼續執行。changeColor
沒有可執行代碼,也沒有再遇到其餘執行上下文了,將其執行上下文從棧頂出棧,回到了 全局執行上下文
中繼續執行。全局執行上下文
。注意:函數中,遇到return能直接終止可執行代碼的執行,所以會直接將當前上下文彈出棧。
使用 ECStack
來模擬調用棧:es5
ECStack=[]
JS第一次執行代碼時就會遇到全局代碼,執行上下文棧會壓入一個全局上下文,咱們用 globalContext
表示它,只有當整個應用程序結束的時候,ECStack
纔會被清空,因此 ECStack
最底部永遠有個 globalContext
:spa
ECStack.push(globalContext)
使用僞代碼模擬上述代碼行爲:
ECStack.push(<changeColor> functionContext); ECStack.push(<swapColors> functionContext); // swapColors出棧 ECStack.pop(); // changeColor出棧 ECStack.pop();
爲了鞏固一下執行上下文的理解,咱們再來繪製一個例子的演變過程,這是一個簡單的閉包例子。
function f1() { var n = 999; function f2() { console.log(n); } return f2; } f1()() // 999
使用僞代碼模擬上述代碼行爲:
ECStack.push(<f1> functionContext); // f1出棧 ECStack.pop(); ECStack.push(<f2> functionContext); // f2出棧 ECStack.pop();
由於f1中的函數f2在f1的可執行代碼中,並無被調用執行,所以執行f1時,f2不會建立新的上下文,而直到f2執行時,才建立了一個新的。具體演變過程以下。
es3版本執行上下文內有三個重要屬性:
能夠將每一個執行上下文抽象爲一個對象。
執行上下文的組成代碼示例:
executionContextObj = { scopeChain: { /* 變量對象(variableObject)+ 全部父執行上下文的變量對象*/ }, [variableObject | activationObject]: { /*函數 arguments/參數,內部變量和函數聲明 */ arguments, ... }, this: {} }
變量對象 是與執行上下文相聯的數據做用域,用來存儲上下文中定義的變量和函數聲明。
不一樣執行上下文中的變量對象也不同:
console.log(this) //window var a=1 //掛到window上的屬性 console.log(window.a) //1 console.log(this.a) //1
活動對象就是變量對象,只不過處於不一樣的狀態和階段而已。
對於 JavaScript
來講做用域及做用域鏈的變量查詢是經過存儲在瀏覽器內存中的執行上下文實現的。當查找變量時,首先從當前上下文中的變量對象查找,若是沒有就會往上查找父級做用域中的變量對象,最終找到全局上下文的變量對象,若是沒有就報錯。這樣由多個執行上下文的變量對象構成的鏈表就叫作做用域鏈。
那麼有同窗就有疑問了,做用域和執行上下文有什麼 區別 呢 :
函數執行上下文是在調用函數時, 函數體代碼執行以前建立,函數調用結束時就會自動釋放。由於不一樣的調用可能有不一樣的參數:
var a = 10; function fn(x) { var a = 20; console.log(arguments) console.log(x) } fn(20) fn(10) // 不一樣的調用可能有不一樣的參數
而JavaScript採用的是詞法做用域,fn 函數建立的做用域在函數定義時就已經肯定了;
關聯 :
做用域只是一個「地盤」,其中沒有變量,要經過做用域對應的執行上下文環境來獲取變量的值,因此做用域是靜態觀念的,而執行上下文環境是動態的。也就是說,做用域只是用於劃分你在這個做用域裏面定義的變量的有效範圍,出了這個做用域就無效。
同一個做用域下,對同一個函數的不一樣的調用會產生不一樣的執行上下文環境,繼而產生不一樣的變量的值,因此,做用域中變量的值是在執行過程當中肯定的,而做用域是在函數建立時就肯定的。
執行上下文的生命週期有三個階段,分別是:
建立階段
生成變量對象
執行階段
生成變量對象
arguments
對象,給變量對象添加形參名稱和值。VO
中,若是 VO
中已經有同名函數,那麼就進行覆蓋(重寫引用指針)。VO
中,而且將變量的值初始化爲undefined
。若是變量的名字已經在變量對象裏存在,不會進行任何操做並繼續掃描。讓咱們舉一個栗子來講明 :
function person(age) { console.log(typeof name); // function console.log(typeof getName); // undefined var name = 'abby'; var hobby = 'game'; var getName = function getName() { return 'Lucky'; }; function name() { return 'Abby'; } function getAge() { return age; } console.log(typeof name); // string console.log(typeof getName); // function name = function () {}; console.log(typeof name); // function } person(20);
在調用person(20)的時候,可是代碼還沒執行的時候,建立的狀態是這樣:
personContext = { scopeChain: { ... }, activationObject: { arguments: { 0: 20, length: 1 }, age: 20, name: pointer, // reference to function name(), getAge: pointer, // reference to function getAge(), hobby: undefined, getName : undefined, }, this: { ... } }
函數在執行以前,會先建立一個函數執行上下文,首先是指出函數的引用,而後按順序對變量進行定義,初始化爲 undefined
存入到 VO
之中,在掃描到變量 name
時發如今 VO
之中存在同名的屬性(函數聲明變量),所以忽略。
全局執行上下文的建立沒有建立 arguments 這一步
創建做用域鏈
在執行期上下文的建立階段,做用域鏈是在變量對象以後建立的。做用域鏈自己包含變量對象。
[[scope]]
表示,它裏面保存父變量對象,因此[[scope]]
就是一條層級鏈。person.[[scope]] = [ globalContext.variableObject ]
personContext = { scopeChain:person.[[scope]] }
personContext = { activationObject: { arguments: { 0: 20, length: 1 }, age: 20, name: pointer, // reference to function name(), getAge: pointer, // reference to function getAge(), hobby: undefined, getName : undefined, }, scopeChain:[activationObject,[[scope]]] }
肯定this的指向
若是當前函數被做爲對象方法調用或使用 bind
、call
、apply
等 API
進行委託調用,則將當前代碼塊的調用者信息(this value)存入當前執行上下文,不然默認爲全局對象調用。
執行階段 中,執行流進入函數而且在上下文中運行/解釋代碼,JS 引擎開始對定義的變量賦值、開始順着做用域鏈訪問變量、若是內部有函數調用就建立一個新的執行上下文壓入執行棧並把控制權交出
此時代碼從上到下執行的時候激活階段的過程是:
console.log
; 此時 name
在 VO
中是函數。getName
未指定值在 VO
中的值是 undefined
。getName
被賦值成函數表達式,name
被賦值爲 abby
console.log
; 此時的 name
因爲函數被字符串賦值覆蓋所以是 string
類型getName
是 function
類型。console.log
; 此時的 name
因爲又被覆蓋所以是 function
類型所以理解執行上下文以後很好解釋了變量提高(Hoisting):實際上變量和函數聲明在代碼裏的位置是不會改變的,而是在編譯階段被JavaScript引擎放入內存中
這就解釋了爲何咱們能在 name
聲明以前訪問它,爲何以後的 name
的類型值發生了變化,爲何 getName
第一次打印的時候是 undefined
等等問題了。
ES6 引入了 let
和 const
關鍵字,從而使 JavaScript 也能像其餘語言同樣擁有了塊級做用域,很好解決了變量提高帶來的一系列問題。
最後執行 console 時候的函數執行上下文:
personContext = { scopeChain: { ... }, activationObject: { arguments: { 0: 20, length: 1 }, age: 20, name: pointer, // reference to function name(), getAge: pointer, // reference to function getAge(), hobby: 'game', getName:pointer, pointer to function getName(), }, this: { ... } }
通常來說當函數執行完成後,當前執行上下文(局部環境)會被彈出執行上下文棧而且等待虛擬機回收,控制權被從新交給執行棧上一層的執行上下文。
示例一:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope();
一、執行全局代碼,生成全局上下文,而且壓入執行棧
ECStack=[ globalContext ] 複製代碼
二、全局上下文初始化
globalContext={ variableObject:[global,scope,checkscope], this:globalContext.variableObject, scopeChain:[globalContext.variableObject] }
三、建立 checkscope 函數時生成內部屬性 [[scope]],並將全局上下文做用域鏈存入其中
checkscope.[[scope]] = [ globalContext.variableObject ]
四、調用 checkscope 函數,建立函數上下文,壓棧
ECStack=[ globalContext, checkscopeContext ]
五、此時 checkscope 函數還未執行,進入執行上下文
checkscopeContext = { activationObject: { arguments: { length: 0 }, scope: undefined, f: pointer, // reference to function f(), }, scopeChain: [activationObject, globalContext.variableObject], this: undefined }
六、checkscope 函數執行,對變量 scope 設值
checkscopeContext = { activationObject: { arguments: { length: 0 }, scope: 'local scope', f: pointer, // reference to function f(), }, scopeChain: [activationObject, globalContext.variableObject], this: undefined }
f 函數被建立生成 [[scope]] 屬性,並保存父做用域的做用域鏈
f.[[scope]]=[ checkscopeContext.activationObject, globalContext.variableObject ]
七、f 函數調用,生成 f 函數上下文,壓棧
ECStack=[ globalContext, checkscopeContext, fContext ]
八、此時 f 函數還未執行,初始化執行上下文
fContext = { activationObject: { arguments: { length: 0 }, }, scopeChain: [fContext.activationObject, checkscopeContext.activationObject, globalContext.variableObject], this: undefined }
九、f 函數執行,沿着做用域鏈查找 scope 值,返回 scope 值
十、f 函數執行完畢,f函數上下文從執行上下文棧中彈出
ECStack=[ globalContext, checkscopeContext ]
十一、checkscope 函數執行完畢,checkscope 執行上下文從執行上下文棧中彈出
ECStack=[ globalContext ]
示例二:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()();
checkscope
函數時生成內部屬性 [[scope]]
,並將全局上下文做用域鏈存入其中checkscope
函數,建立函數上下文,壓棧此時 checkscope
函數還未執行,進入執行上下文
[[scope]]
屬性建立做用域鏈arguments
屬性建立活動對象checkscope
函數執行,對變量 scope
設值,f
函數被建立生成 [[scope]]
屬性,並保存父做用域的做用域鏈f
函數調用,生成 f
函數上下文,壓棧此時 f
函數還未執行,初始化執行上下文
[[scope]]
屬性建立做用域鏈arguments
屬性建立活動對象f
函數執行,沿着做用域鏈查找 scope
值,返回 scope
值f
函數執行完畢,f
函數上下文從執行上下文棧中彈出
能夠看到和前面惟一的區別就是 checkScope
函數執行完先出棧了,以後再執行 f
函數,步驟與示例一一致
fContext = { scopeChain: [activationObject, checkscopeContext.activationObject, globalContext.variableObject], }
這裏在 checkscopeContext
函數執行完銷燬後,f
函數依然能夠讀取到 checkscopeContext.AO
的值,也就是說 checkscopeContext.AO
依然活在內存中,f
函數依然能夠經過 f
函數的做用域鏈找到它。而爲何 checkscopeContext.AO
沒有被銷燬,正是由於 f
函數引用了 checkscopeContext.AO
中的值,又正是由於JS實現了在子上下文引用父上下文的變量的時候,不會銷燬這些變量的效果實現了閉包 這個概念!
ES5 規範去除了 ES3 中變量對象和活動對象,以 詞法環境組件( LexicalEnvironment component) 和 變量環境組件( VariableEnvironment component) 替代。
es5 執行上下文的生命週期也包括三個階段:建立階段 → 執行階段 → 回收階段
建立階段作了三件事:
僞代碼大概以下:
ExecutionContext = { ThisBinding = <this value>, // 肯定this LexicalEnvironment = { ... }, // 詞法環境 VariableEnvironment = { ... }, // 變量環境 }
ThisBinding
是和執行上下文綁定的,也就是說每一個執行上下文中都有一個 this
,與 es3
的this
並無什麼區別,this
的值是在執行的時候才能確認,定義的時候不能確認
詞法環境的結構以下:
GlobalExectionContext = { // 全局執行上下文 LexicalEnvironment: { // 詞法環境 EnvironmentRecord: { // 環境記錄 Type: "Object", // 全局環境 // 標識符綁定在這裏 outer: <null> // 對外部環境的引用 } } FunctionExectionContext = { // 函數執行上下文 LexicalEnvironment: { // 詞法環境 EnvironmentRecord: { // 環境記錄 Type: "Declarative", // 函數環境 // 標識符綁定在這裏 // 對外部環境的引用 outer: <Global or outer function environment reference> } }
能夠看到詞法環境有兩種類型 :
null
。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,this
的值指向這個全局對象。arguments
對象。對外部環境的引用能夠是全局環境,也能夠是包含內部函數的外部函數環境。詞法環境有兩個組件 :
環境記錄器也有兩種類型 :
在全局環境中使用 對象環境記錄器,用來定義出如今全局上下文中的變量和函數的關係。
所以:
outer
值爲 null
;outer
值爲全局對象,或者爲父級詞法環境(做用域)變量環境也是一個詞法環境,所以它具備上面定義的詞法環境的全部屬性。
在 ES6 中,詞法環境和 變量環境的區別在於前者用於存儲函數聲明和變量( let
和const
關鍵字)綁定,然後者僅用於存儲變量( var
)綁定,所以變量環境實現函數級做用域,經過詞法環境在函數做用域的基礎上實現塊級做用域。
🚨 使用 let
/ const
聲明的全局變量,會被綁定到 Script
對象而不是 Window
對象,不能以Window.xx
的形式使用;使用 var
聲明的全局變量會被綁定到 Window
對象;使用 var
/ let
/ const
聲明的局部變量都會被綁定到 Local
對象。注:Script
對象、Window
對象、Local
對象三者是平行並列關係。
箭頭函數沒有本身的上下文,沒有arguments,也不存在變量提高
使用例子進行介紹
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30);
遇到調用函數 multiply
時,函數執行上下文開始被建立:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符綁定在這裏 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 標識符綁定在這裏 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符綁定在這裏 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 標識符綁定在這裏 g: undefined }, outer: <GlobalLexicalEnvironment> } }
變量提高的緣由:在建立階段,函數聲明存儲在環境中,而變量會被設置爲 undefined
(在 var 的狀況下)或保持未初始化 uninitialized
(在 let 和 const 的狀況下)。因此這就是爲何能夠在聲明以前訪問 var 定義的變量(儘管是 undefined ),但若是在聲明以前訪問 let 和 const 定義的變量就會提示引用錯誤的緣由。這就是所謂的變量提高。
圖解變量提高:
var myname = "極客時間" function showName(){ console.log(myname); if(0){ var myname = "極客邦" } console.log(myname); } showName()
在 showName 內部查找 myname 時會先使用當前函數執行上下文裏面的變量 myname ,因爲變量提高
,當前的執行上下文中就包含了變量 myname,而值是 undefined,因此獲取到的 myname 的值就是 undefined。
在此階段,完成對全部這些變量的分配,最後執行代碼,若是 JavaScript
引擎不能在源碼中聲明的實際位置找到 let
變量的值,它會被賦值爲 undefined
執行上下文出棧等待虛擬機回收執行上下文
對象環境記錄器
,接着建立他的外部環境引用 outer
,值爲 null聲明式環境記錄器
,接着建立他的外部環境引用 outer
,值爲 null,值爲全局對象,或者爲父級詞法環境將詞法環境中 outer
抽離出來,執行上下文結構以下:
下面咱們以以下示例來分析執行上下文的建立及執行過程:
function foo(){ var a = 1 let b = 2 { let b = 3 var c = 4 let d = 5 console.log(a) console.log(b) } console.log(b) console.log(c) console.log(d) } foo()
第一步: 調用 foo
函數前先編譯並建立執行上下文,在編譯階段將 var
聲明的變量存放到變量環境中,let
聲明的變量存放到詞法環境中,須要注意的是此時在函數體內部塊做用域中 let
聲明的變量不會被存放到詞法環境中,以下圖所示 :
第二步: 繼續執行代碼,當執行到代碼塊裏面時,變量環境中的 a 的值已經被設置爲1,詞法環境中 b 的值已經被設置成了2,此時函數的執行上下文如圖所示:
從圖中就能夠看出,當進入函數的做用域塊時,做用域塊中經過 let
聲明的變量,會被存放在詞法環境的一個單獨的區域中,這個區域中的變量並不影響做用域塊外面的變量,所以示例中在函數體內塊做用域中聲明的變量的 b 與函數做用域中聲明的變量 b 都是獨立的存在。
在詞法環境內部,實際上維護了一個小型棧結構,棧底是函數最外層的變量,進入一個做用域塊後,就會把該做用域內部的變量壓到棧頂;當該塊級做用域執行完成以後,該做用域的信息就會從棧頂彈出,這就是詞法環境的結構。
第三步: 當代碼執行到做用域塊中的 console.log(a)
時,就須要在詞法環境和變量環境中查找變量 a 的值了,具體查找方式是:沿着詞法環境的棧頂向下查詢,若是在詞法環境中的某個塊中查找到了,就直接返回給 JavaScript 引擎,若是沒有查找到,那麼繼續在變量環境中查找。
這樣一個變量查找過程就完成了,你能夠參考下圖:
第四步: 當函數體內塊做用域執行結束以後,其內部變量就會從詞法環境的棧頂彈出,此時執行上下文以下圖所示:
第五步: 當foo函數執行完畢後執行棧將foo函數的執行上下文彈出。
因此,塊級做用域就是經過詞法環境的棧結構來實現的,而變量提高是經過變量環境來實現,經過這二者的結合,JavaScript 引擎也就同時支持了變量提高和塊級做用域了。
outer 是一個外部引用,用來指向外部的執行上下文,其是由詞法做用域指定的
function bar() { console.log(myName) } function foo() { var myName = " 極客邦 " bar() } var myName = " 極客時間 " foo()
當一段代碼使用了一個變量時,JavaScript 引擎首先會在「當前的執行上下文」中查找該變量, 好比上面那段代碼在查找 myName 變量時,若是在當前的變量環境中沒有查找到,那麼 JavaScript 引擎會繼續在 outer 所指向的執行上下文中查找。爲了直觀理解,你能夠看下面這張圖:
從圖中能夠看出,bar 函數和 foo 函數的 outer 都是指向全局上下文的,這也就意味着若是在 bar 函數或者 foo 函數中使用了外部變量,那麼 JavaScript 引擎會去全局執行上下文中查找。咱們把這個查找的鏈條就稱爲做用域鏈。 如今你知道變量是經過做用域鏈來查找的了,不過還有一個疑問沒有解開,foo 函數調用的 bar 函數,那爲何 bar 函數的外部引用是全局執行上下文,而不是 foo 函數的執行上下文?
這是由於在 JavaScript 執行過程當中,其做用域鏈是由詞法做用域決定的。詞法做用域指做用域是由代碼中函數聲明的位置來決定的,所以是靜態的做用域
結合變量環境、詞法環境以及做用域鏈,咱們看下下面的代碼:
function bar() { var myName = " 極客世界 " let test1 = 100 if (1) { let myName = "Chrome 瀏覽器 " console.log(test) } } function foo() { var myName = " 極客邦 " let test = 2 { let test = 3 bar() } } var myName = " 極客時間 " let myAge = 10 let test = 1 foo()
對於上面這段代碼,當執行到 bar 函數內部的 if 語句塊時,其調用棧的狀況以下圖所示:
解釋下這個過程。首先是在 bar 函數的執行上下文中查找,但由於 bar 函數的執行上下文中沒有定義 test 變量,因此根據詞法做用域的規則,下一步就在 bar 函數的外部做用域中查找,也就是全局做用域。