摘要:代碼執行階段在這個階段會生成三個重要的東西變量對象,作用域鏈變量對象在函數上下文中,我們用活動對象來表示變量對象。這時候執行上下文的作用域鏈,我們命名為至此,作用域鏈創建完畢。
好久沒更新文章了,這一憋就是一個大的。
說起js中的概念,執行上下文和作用域應該是大家最容易混淆的,你說混淆就混淆吧,其實大多數人在開發的時候不是很關注這兩個名詞,但是這里面偏偏還夾雜好多其他的概念--變量提升啊,閉包啊,this啊!
因此,搞明白這兩者的關系對深入javascript至關重要
JavaScript代碼的整個執行過程,分為兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段作用域規則會確定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段創建。
上面提到的可執行代碼,那么什么是可執行代碼呢?
其實很簡單,就三種,全局代碼、函數代碼、eval代碼。
其中eval代碼大家可以忽略,畢竟實際開發中處于性能考慮基本不會用到,所以接下來我們重點關注的就是全局代碼、函數代碼
在龐大的代碼里必然不會只有一兩個函數,那么如何管理每次執行函數時候創建的上下文呢
js引擎創建了執行上下文棧(Execution context stack,ECS)來管理執行上下文
為了模擬執行上下文棧的行為,讓我們定義執行上下文棧是一個數組:
ECStack = [];
試想當js開始要解釋執行代碼的時候,最先遇到的就是全局代碼,所以初始化的時候首先就會向執行上下文棧壓入一個全局執行上下文,我們用 globalContext 表示它,并且只有當整個應用程序結束的時候,ECStack 才會被清空,所以 ECStack 最底部永遠有個 globalContext:
ECStack = [ globalContext ];
舉個?:
function out(){ function inner(){} inner() } out()
那么這個函數的執行上下文棧會經歷以下過程:
ECStack.push(globalContext) ECStack.push(outContext) ECStack.push(innerContext) ECStack.pop(innerContext) ECStack.pop(outContext) ECStack.pop(globalContext)
再來看一個閉包的?:
function f1(){ var n=999; function f2(){ console.log(n) } return f2; } var result=f1(); result(); // 999
該函數的執行上下文棧會經歷以下過程:
ECStack.push(globalContext) ECStack.push(f1Context) ECStack.pop(f1Context) ECStack.push(resultContext) ECStack.pop(resultContext) ECStack.pop(globalContext)
大家自行感受一下對比,一定要記住上下文是在函數調用的時候才會生產
既然調用一個函數時一個新的執行上下文會被創建。那執行上下文的生命周期同樣可以分為兩個階段。
創建階段
在這個階段中,執行上下文會分別創建變量對象,建立作用域鏈,以及確定this的指向。
代碼執行階段
在這個階段會生成三個重要的東西
a.變量對象(Variable object,VO)
b.作用域鏈(Scope chain)
c.this
變量對象
在函數上下文中,我們用活動對象(activation object, AO)來表示變量對象。
活動對象其實就是被激活的變量對象,只是變量對象是規范上的或者說是引擎實現上的,不可在 JavaScript 環境中訪問,只有到當進入一個執行上下文中,這個執行上下文的變量對象才會被激活,所以才叫 activation object,而只有活動對象上的各種屬性才能被訪問。
執行上下文的代碼會分成兩個階段進行處理:分析(進入)和執行
進入執行上下文
當進入執行上下文時,這時候還沒有執行代碼,
變量對象會包括:
函數的所有形參 (如果是函數上下文)
a.由名稱和對應值組成的一個變量對象的屬性被創建
b.沒有實參,屬性值設為 undefined
函數聲明
a.由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建
b.如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性
變量聲明
a.由名稱和對應值(undefined)組成一個變量對象的屬性被創建;
b.如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
根據這個規則,理解變量提升就變得十分簡單了
舉個?分析下,看下面的代碼:
function foo(a) { console.log(b) console.log(c) var b = 2; function c() {} var d = function() {}; b = 3; } foo(1);
在進入執行上下文后,這時候的 AO 是:
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined }
代碼執行
在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值
還是上面的例子,當代碼執行完后,這時候的 AO 是:
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: reference to function c(){}, d: reference to FunctionExpression "d" }
因此,這個例子代碼執行順序就是這樣的
function foo(a) { var b function c() {} var d console.log(b) console.log(c) b = 2; function c() {} d = function() {}; b = 3; }作用域
作用域規定了如何查找變量,也就是確定當前執行代碼對變量的訪問權限。
JavaScript 采用詞法作用域(lexical scoping),也就是靜態作用域。
因為 JavaScript 采用的是詞法作用域,函數的作用域在函數定義的時候就決定了。
而與詞法作用域相對的是動態作用域,函數的作用域是在函數調用的時候才決定的。
經典的一道面試題
var a = 1 function out(){ var a = 2 inner() } function inner(){ console.log(a) } out() //====> 1作用域鏈
當查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈
下面,讓我們以一個函數的創建和激活兩個時期來講解作用域鏈是如何創建和變化的。
上面講到函數作用域是在創建的階段確定
這是因為函數有一個內部屬性 [[scope]],當函數創建的時候,就會保存所有父變量對象到其中,你可以理解 [[scope]] 就是所有父變量對象的層級鏈,但是注意:[[scope]] 并不代表完整的作用域鏈!
舉個?
function out() { function inner() { ... } }
函數創建時,各自的[[scope]]為:
out.[[scope]] = [ globalContext.VO ]; inner.[[scope]] = [ outContext.AO, globalContext.VO ];
當函數激活時,進入函數上下文,創建 AO 后,就會將活動對象添加到作用鏈的前端。
這時候執行上下文的作用域鏈,我們命名為 Scope:
Scope = [AO].concat([[Scope]]);
至此,作用域鏈創建完畢。
最后我們用一個代碼完整的說明下整個過程
var scope = "global scope"; function checkscope(){ var scope2 = "local scope"; return scope2; } checkscope();
執行過程如下:
1.checkscope 函數被創建,保存作用域鏈到 內部屬性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
2.執行 checkscope 函數,創建 checkscope 函數執行上下文,checkscope 函數執行上下文被壓入執行上下文棧
ECStack = [ checkscopeContext, globalContext ];
3.checkscope 函數并不立刻執行,開始做準備工作,第一步:復制函數[[scope]]屬性創建作用域鏈
checkscopeContext = { Scope: checkscope.[[scope]], }
4.第二步:用 arguments 創建活動對象,隨后初始化活動對象,加入形參、函數聲明、變量聲明
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: checkscope.[[scope]], }
5.第三步:將活動對象壓入 checkscope 作用域鏈頂端
checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] }
6.準備工作做完,開始執行函數,隨著函數的執行,修改 AO 的屬性值
ECStack = [ globalContext ];
至此,關于作用域和執行上下文的介紹就到這里,希望大家多消化,有問題請在評論中及時指出
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93598.html
摘要:之前一篇文章我們詳細說明了變量對象,而這里,我們將詳細說明作用域鏈。而的作用域鏈,則同時包含了這三個變量對象,所以的執行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當前的函數調用棧,為當前正在被執行的函數的作用域鏈,為當前的局部變量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初學JavaScrip...
摘要:理解了這句話,我們就可以來看閉包了閉包前面說過,函數可以訪問函數作用域鏈中的變量,但如果我們想在函數外訪問函數內卻不行了。 不管是閉包還是this關鍵字,都是困擾JS初學者的比較難懂的東西,如果你對它們的認識還不足夠清晰,那么現在就一起把它們掌握掉。還是那句話,我們從最基本的開始,建立起一個非常清晰的知識結構,好了,開始吧 ? 閉包 當然我們今天說的是javascript里的閉包。要學...
摘要:全局和上下文中的作用域鏈這里不一定很有趣,但必須要提示一下。全局上下文的作用域鏈僅包含全局對象。代碼的上下文與當前的調用上下文擁有同樣的作用域鏈。代碼執行時對作用域鏈的影響在中,在代碼執行階段有兩個聲明能修改作用域鏈。 1 定義 我們已經知道一個執行上下文中的數據(參數,變量,函數)作為屬性存儲在變量對象中。 也知道變量對象是在每次進入上下文是創建并填入初始值,值的更新出現在代碼執行階...
摘要:一看這二逼就是周杰倫的死忠粉看看控制臺輸出,確實沒錯就是對象。從根本上來說,作用域是基于函數的,而執行環境是基于對象的例如全局執行環境即全局對象。全局對象全局屬性和函數可用于所有內建的對象。全局對象只是一個對象,而不是類。 覺得本人寫的不算很爛的話,可以登錄關注一下我的GitHub博客,博客會堅持寫下去。 今天同學去面試,做了兩道面試題,全部做錯了,發過來給我看,我一眼就看出來了,因為...
摘要:回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用于對該事件或條件進行響應。若是使用回調函數進行處理,代碼就可以繼續進行其他任務,而無需空等。參考理解回調函數理解與使用中的回調函數這篇相當不錯回調函數 為什么寫回調函數 對于javascript中回調函數 一直處于理解,但是應用不好的階段,總是在別人家的代碼中看到很巧妙的回調,那時候會有wow c...
閱讀 2910·2021-10-27 14:19
閱讀 540·2021-10-18 13:29
閱讀 1134·2021-07-29 13:56
閱讀 3553·2019-08-30 13:19
閱讀 1932·2019-08-29 12:50
閱讀 1056·2019-08-23 18:16
閱讀 3525·2019-08-22 15:37
閱讀 1903·2019-08-22 15:37