摘要:執(zhí)行上下文棧首先我們先了解一下什么是執(zhí)行上下文棧。那么隨著我們的執(zhí)行上下文數(shù)量的增加,引擎又如何去管理這些執(zhí)行上下文呢這時便有了執(zhí)行上下文棧。這樣由多個執(zhí)行上下文的變量對象構(gòu)成的鏈表就叫做作用域鏈。
執(zhí)行上下文棧
首先我們先了解一下什么是執(zhí)行上下文棧(Execution context stack)。
上面這張圖來自于mdn,分別展示了棧、堆和隊列,其中棧就是我們所說的執(zhí)行上下文棧;堆是用于存儲對象這種復(fù)雜類型,我們復(fù)制對象的地址引用就是這個堆內(nèi)存的地址;隊列就是異步隊列,用于event loop的執(zhí)行。
JS代碼在引擎中是以“一段一段”的方式來分析執(zhí)行的,而并非一行一行來分析執(zhí)行。而這“一段一段”的可執(zhí)行代碼無非為三種:Global code、Function Code、Eval code。這些可執(zhí)行代碼在執(zhí)行的時候又會創(chuàng)建一個一個的執(zhí)行上下文(Execution context)。例如,當執(zhí)行到一個函數(shù)的時候,JS引擎會做一些“準備工作”,而這個“準備工作”,我們稱其為執(zhí)行上下文。
那么隨著我們的執(zhí)行上下文數(shù)量的增加,JS引擎又如何去管理這些執(zhí)行上下文呢?這時便有了執(zhí)行上下文棧。
這里我用一段貫穿全文的例子來講解執(zhí)行上下文棧的執(zhí)行過程:
var scope = "global scope"; function checkscope(s) { var scope = "local scope"; function f() { return scope; } return f(); } checkscope("scope");
當JS引擎去解析代碼的時候,最先碰到的就是Global code,所以一開始初始化的時候便會將全局上下文推入執(zhí)行上下文棧,并且只有在整個應(yīng)用程序執(zhí)行完畢的時候,全局上下文才會推出執(zhí)行上下文棧。
這里我們用ECS來模擬執(zhí)行上下文棧,用globalContext來表示全局上下文:
ESC = [ globalContext, // 一開始只有全局上下文 ]
然后當代碼執(zhí)行checkscope函數(shù)的時候,會創(chuàng)建checkscope函數(shù)的執(zhí)行上下文,并將其壓入執(zhí)行上下文棧:
ESC = [ checkscopeContext, // checkscopeContext入棧 globalContext, ]
接著代碼執(zhí)行到return f()的時候,f函數(shù)的執(zhí)行上下文被創(chuàng)建:
ESC = [ fContext, // fContext入棧 checkscopeContext, globalContext, ]
f函數(shù)執(zhí)行完畢后,f函數(shù)的執(zhí)行上下文出棧,隨后checkscope函數(shù)執(zhí)行完畢,checkscope函數(shù)的執(zhí)行上下文出棧:
// fContext出棧 ESC = [ // fContext出棧 checkscopeContext, globalContext, ] // checkscopeContext出棧 ESC = [ // checkscopeContext出棧 globalContext, ]變量對象
每一個執(zhí)行上下文都有三個重要的屬性:
變量對象
作用域鏈
this
這一節(jié)我們先來說一下變量對象(Variable object,這里簡稱VO)。
變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲了在上下文中定義的變量和函數(shù)聲明。并且不同的執(zhí)行上下文也有著不同的變量對象,這里分為全局上下文中的變量對象和函數(shù)執(zhí)行上下文中的變量對象。
全局上下文中的變量對象全局上下文中的變量對象其實就是全局對象。我們可以通過this來訪問全局對象,并且在瀏覽器環(huán)境中,this === window;在node環(huán)境中,this === global。
函數(shù)上下文中的變量對象在函數(shù)上下文中的變量對象,我們用活動對象來表示(activation object,這里簡稱AO),為什么稱其為活動對象呢,因為只有到當進入一個執(zhí)行上下文中,這個執(zhí)行上下文的變量對象才會被激活,并且只有被激活的變量對象,其屬性才能被訪問。
在函數(shù)執(zhí)行之前,會為當前函數(shù)創(chuàng)建執(zhí)行上下文,并且在此時,會創(chuàng)建變量對象:
根據(jù)函數(shù)arguments屬性初始化arguments對象;
根據(jù)函數(shù)聲明生成對應(yīng)的屬性,其值為一個指向內(nèi)存中函數(shù)的引用指針。如果函數(shù)名稱已存在,則覆蓋;
根據(jù)變量聲明生成對應(yīng)的屬性,此時初始值為undefined。如果變量名已聲明,則忽略該變量聲明;
還是以剛才的代碼為例:
var scope = "global scope"; function checkscope(s) { var scope = "local scope"; function f() { return scope; } return f(); } checkscope("scope");
在執(zhí)行checkscope函數(shù)之前,會為其創(chuàng)建執(zhí)行上下文,并初始化變量對象,此時的變量對象為:
VO = { arguments: { 0: "scope", length: 1, }, s: "scope", // 傳入的參數(shù) f: pointer to function f(), scope: undefined, // 此時聲明的變量為undefined }
隨著checkscope函數(shù)的執(zhí)行,變量對象被激活,變相對象內(nèi)的屬性隨著代碼的執(zhí)行而改變:
VO = { arguments: { 0: "scope", length: 1, }, s: "scope", // 傳入的參數(shù) f: pointer to function f(), scope: "local scope", // 變量賦值 }
其實也可以用另一個概念“函數(shù)提升”和“變量提升”來解釋:
function checkscope(s) { function f() { // 函數(shù)提升 return scope; } var scope; // 變量聲明提升 scope = "local scope" // 變量對象的激活也相當于此時的變量賦值 return f(); }作用域鏈
每一個執(zhí)行上下文都有三個重要的屬性:
變量對象
作用域鏈
this
這一節(jié)我們說一下作用域鏈。
什么是作用域鏈當查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到,就會從父級執(zhí)行上下文的變量對象中查找,一直找到全局上下文的變量對象。這樣由多個執(zhí)行上下文的變量對象構(gòu)成的鏈表就叫做作用域鏈。
下面還是用我們的例子來講解作用域鏈:
var scope = "global scope"; function checkscope(s) { var scope = "local scope"; function f() { return scope; } return f(); } checkscope("scope");
首先在checkscope函數(shù)聲明的時候,內(nèi)部會綁定一個[[scope]]的內(nèi)部屬性:
checkscope.[[scope]] = [ globalContext.VO ];
接著在checkscope函數(shù)執(zhí)行之前,創(chuàng)建執(zhí)行上下文checkscopeContext,并推入執(zhí)行上下文棧:
復(fù)制函數(shù)的[[scope]]屬性初始化作用域鏈;
創(chuàng)建變量對象;
將變量對象壓入作用域鏈的最頂端;
// -> 初始化作用域鏈; checkscopeContext = { scope: checkscope.[[scope]], } // -> 創(chuàng)建變量對象 checkscopeContext = { scope: checkscope.[[scope]], VO = { arguments: { 0: "scope", length: 1, }, s: "scope", // 傳入的參數(shù) f: pointer to function f(), scope: undefined, // 此時聲明的變量為undefined }, } // -> 將變量對象壓入作用域鏈的最頂端 checkscopeContext = { scope: [VO, checkscope.[[scope]]], VO = { arguments: { 0: "scope", length: 1, }, s: "scope", // 傳入的參數(shù) f: pointer to function f(), scope: undefined, // 此時聲明的變量為undefined }, }
接著,隨著函數(shù)的執(zhí)行,修改變量對象:
checkscopeContext = { scope: [VO, checkscope.[[scope]]], VO = { arguments: { 0: "scope", length: 1, }, s: "scope", // 傳入的參數(shù) f: pointer to function f(), scope: "local scope", // 變量賦值 } }
與此同時遇到f函數(shù)聲明,f函數(shù)綁定[[scope]]屬性:
checkscope.[[scope]] = [ checkscopeContext.VO, // f函數(shù)的作用域還包括checkscope的變量對象 globalContext.VO ];
之后f函數(shù)的步驟同checkscope函數(shù)。
再來一個經(jīng)典的例子:
var data = []; for (var i = 0; i < 6; i++) { data[i] = function () { console.log(i); }; } data[0](); // ...
很簡單,不管訪問data幾,最終console打印出來的都是6,因為在ES6之前,JS都沒有塊級作用域的概念,for循環(huán)內(nèi)的代碼都在全局作用域下。
在data函數(shù)執(zhí)行之前,此時全局上下文的變量對象為:
globalContext.VO = { data: [pointer to function ()], i: 6, // 注意:此時的i值為6 }
每一個data匿名函數(shù)的執(zhí)行上下文鏈大致都如下:
data[n]Context = { scope: [VO, globalContext.VO], VO: { arguments: { length: 0, } } }
那么在函數(shù)執(zhí)行的時候,會先去自己匿名函數(shù)的變量對象上找i的值,發(fā)現(xiàn)沒有后會沿著作用域鏈查找,找到了全局執(zhí)行上下文的變量對象,而此時全局執(zhí)行上下文的變量對象中的i為6,所以每一次都打印的是6了。
詞法作用域 & 動態(tài)作用域JavaScript這門語言是基于詞法作用域來創(chuàng)建作用域的,也就是說一個函數(shù)的作用域在函數(shù)聲明的時候就已經(jīng)確定了,而不是函數(shù)執(zhí)行的時候。
改一下之前的例子:
var scope = "global scope"; function f() { console.log(scope) } function checkscope() { var scope = "local scope"; f(); } checkscope();
因為JavaScript是基于詞法作用域創(chuàng)建作用域的,所以打印的結(jié)果是global scope而不是local scope。我們結(jié)合上面的作用域鏈來分析一下:
首先遇到了f函數(shù)的聲明,此時為其綁定[[scope]]屬性:
// 這里就是我們所說的“一個函數(shù)的作用域在函數(shù)聲明的時候就已經(jīng)確定了” f.[[scope]] = [ globalContext.VO, // 此時的全局上下文的變量對象中保存著scope = "global scope"; ];
然后我們直接跳過checkscope的執(zhí)行上下文的創(chuàng)建和執(zhí)行的過程,直接來到f函數(shù)的執(zhí)行上。此時在函數(shù)執(zhí)行之前初始化f函數(shù)的執(zhí)行上下文:
// 這里就是為什么會打印global scope fContext = { scope: [VO, globalContext.VO], // 復(fù)制f.[[scope]],f.[[scope]]只有全局執(zhí)行上下文的變量對象 VO = { arguments: { length: 0, }, }, }
然后到了f函數(shù)執(zhí)行的過程,console.log(scope),會沿著f函數(shù)的作用域鏈查找scope變量,先是去自己執(zhí)行上下文的變量對象中查找,沒有找到,然后去global執(zhí)行上下文的變量對象上查找,此時scope的值為global scope。
this在這里this綁定也可以分為全局執(zhí)行上下文和函數(shù)執(zhí)行上下文:
在全局執(zhí)行上下文中,this的指向全局對象。(在瀏覽器中,this引用 Window 對象)。
在函數(shù)執(zhí)行上下文中,this 的值取決于該函數(shù)是如何被調(diào)用的。如果它被一個引用對象調(diào)用,那么this會被設(shè)置成那個對象,否則this的值被設(shè)置為全局對象或者undefined(在嚴格模式下)
總結(jié)起來就是,誰調(diào)用了,this就指向誰。
執(zhí)行上下文這里,根據(jù)之前的例子來完整的走一遍執(zhí)行上下文的流程:
var scope = "global scope"; function checkscope(s) { var scope = "local scope"; function f() { return scope; } return f(); } checkscope("scope");
首先,執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文,并且全局執(zhí)行上下文進入執(zhí)行上下文棧:
globalContext = { scope: [globalContext.VO], VO: global, this: globalContext.VO } ESC = [ globalContext, ]
然后隨著代碼的執(zhí)行,走到了checkscope函數(shù)聲明的階段,此時綁定[[scope]]屬性:
checkscope.[[scope]] = [ globalContext.VO, ]
在checkscope函數(shù)執(zhí)行之前,創(chuàng)建checkscope函數(shù)的執(zhí)行上下文,并且checkscope執(zhí)行上下文入棧:
// 創(chuàng)建執(zhí)行上下文 checkscopeContext = { scope: [VO, globalContext.VO], // 復(fù)制[[scope]]屬性,然后VO推入作用域鏈頂端 VO = { arguments: { 0: "scope", length: 1, }, s: "scope", // 傳入的參數(shù) f: pointer to function f(), scope: undefined, }, this: globalContext.VO, } // 進入執(zhí)行上下文棧 ESC = [ checkscopeContext, globalContext, ]
checkscope函數(shù)執(zhí)行,更新變量對象:
// 創(chuàng)建執(zhí)行上下文 checkscopeContext = { scope: [VO, globalContext.VO], // 復(fù)制[[scope]]屬性,然后VO推入作用域鏈頂端 VO = { arguments: { 0: "scope", length: 1, }, s: "scope", // 傳入的參數(shù) f: pointer to function f(), scope: "local scope", // 更新變量 }, this: globalContext.VO, }
f函數(shù)聲明,綁定[[scope]]屬性:
f.[[scope]] = [ checkscopeContext.VO, globalContext.VO, ]
f函數(shù)執(zhí)行,創(chuàng)建執(zhí)行上下文,推入執(zhí)行上下文棧:
// 創(chuàng)建執(zhí)行上下文 fContext = { scope: [VO, checkscopeContext.VO, globalContext.VO], // 復(fù)制[[scope]]屬性,然后VO推入作用域鏈頂端 VO = { arguments: { length: 0, }, }, this: globalContext.VO, } // 入棧 ESC = [ fContext, checkscopeContext, globalContext, ]
f函數(shù)執(zhí)行完成,f函數(shù)執(zhí)行上下文出棧,checkscope函數(shù)執(zhí)行完成,checkscope函數(shù)出棧:
ESC = [ // fContext出棧 checkscopeContext, globalContext, ] ESC = [ // checkscopeContext出棧, globalContext, ]
到此,一個整體的執(zhí)行上下文的流程就分析完了。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/109683.html
摘要:閉包就好像從中分離出來的一個充滿神秘色彩的未開化世界,只有最勇敢的人才能到達那里。興奮地趕緊自測咔咔咔連點三下。結(jié)果當時內(nèi)心表情大概就像上面這個哥們。但還是在工位上故作鎮(zhèn)定地趕緊百度了下。 ? 閉包就好像從JavaScript中分離出來的一個充滿神秘色彩的未開化世界,只有最勇敢的人才能到達那里。——《你不知道的JavaScript 上卷》 1、起源 js閉包很長一...
摘要:本文是面向前端小白的,大手子可以跳過,寫的不好之處多多分鐘搞定常用基礎(chǔ)知識前端掘金基礎(chǔ)智商劃重點在實際開發(fā)中,已經(jīng)非常普及了。 JavaScript字符串所有API全解密 - 掘金關(guān)于 我的博客:louis blog SF專欄:路易斯前端深度課 原文鏈接:JavaScript字符串所有API全解密 本文近 6k 字,讀完需 10 分鐘。 字符串作為基本的信息交流的橋梁,幾乎被所有的編程...
摘要:所以我們今天只談前端加密,一個部分人認為沒有意義的工作。在中,認證過程使用了非對稱加密算法,非認證過程中使用了對稱加密算法。非對稱加密上文中我們討論了前端的哈希加密以及應(yīng)用的場景。 showImg(https://segmentfault.com/img/bVAhTC); 當然在談安全。 前端安全是Web安全的一部分,常見的安全問題會有XSS、CSRF、SQL注入等,然而這些已經(jīng)在程師...
摘要:在這個案例里,這些是欺騙性的功能,它們似乎有一個唯一目的,即混淆自動檢測系統(tǒng),反病毒軟件,或者那些甚至嘗試手工分析這些程序樣本的分析人員。受害機器的處于所規(guī)定的地址空間,攻擊者是無法通過到達的。 初始傳染手段?-?Nuclear?Pack 已經(jīng)有一些其他的文章介紹過Nuclear?Pack破解工具包。可能它還不像g10pack或者BlackHole這些工具那么流行,也沒有像CoolE...
摘要:每次調(diào)用函數(shù)時,都會創(chuàng)建一個新的執(zhí)行上下文。理解執(zhí)行上下文和堆棧可以讓您了解代碼為什么要計算您最初沒有預(yù)料到的不同值的原因。 首發(fā):https://www.love85g.com/?p=1723 在這篇文章中,我將深入研究JavaScript最基本的部分之一,即執(zhí)行上下文。在這篇文章的最后,您應(yīng)該更清楚地了解解釋器要做什么,為什么在聲明一些函數(shù)/變量之前可以使用它們,以及它們的值是如何...
閱讀 2102·2023-05-11 16:55
閱讀 3503·2021-08-10 09:43
閱讀 2617·2019-08-30 15:44
閱讀 2440·2019-08-29 16:39
閱讀 583·2019-08-29 13:46
閱讀 2004·2019-08-29 13:29
閱讀 921·2019-08-29 13:05
閱讀 691·2019-08-26 13:51