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