1. 程式人生 > >JavaScript的執行上下文,真沒你想的那麼難

JavaScript的執行上下文,真沒你想的那麼難

> 作者:小土豆 > 部落格園:https://www.cnblogs.com/HouJiao/ > 掘金:https://juejin.im/user/2436173500265335 # 前言 在正文開始前,先來看兩個`JavaScript`程式碼片段。 #### 程式碼一 ```javascript console.log(a); var a = 10; ``` #### 程式碼二 ```javascript fn1(); fn2(); function fn1(){ console.log('fn1'); } var fn2 = function(){ console.log('fn2'); } ``` 如果你能正確的`回答`並`解釋`以上程式碼的`輸出結果`,那說明你對`JavaScript`的`執行上下文`已經有一定的瞭解;反之,閱讀完這篇文章,相信你一定會得到答案。 # 什麼是執行上下文 ```javascript var a = 10; function fn1(){ console.log(a); // 10 function test(){ console.log('test'); } } fn1(); test(); // Uncaught ReferenceError: test is not defined ``` 上面這段程式碼我們在`全域性環境`中定義了變數`a`和函式`fn1`,在呼叫函式`fn1`時,`fn1`內部可以成功訪問`全域性環境`中定義的變數`a`;接著,我們在`全域性環境`中呼叫了`fn1`內部定義的`test`函式,這行程式碼會導致`ReferenceError`,因為我們在`全域性環境`中無法訪問`fn1`內部的`test`函式。那這些`變數`或者`函式`能否正常被訪問,就和`JavaScript`的`執行上下文`有著很大的關係。 `JavaScript`的`執行上下文`也叫`JavaScript`的`執行環境`,它是在`JavaScript`程式碼的執行過程中創建出來的,它規定了當前程式碼能訪問到的`變數`和`函式`,同時也支援著整個`JavaScript`程式碼的執行。 在一段程式碼的執行過程中,如果是執行`全域性環境`中的程式碼,則會建立一個`全域性執行上下文`,如果遇到`函式`,則會建立一個`函式執行上下文`。 ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a8ff9087f5374d51aaed4f81947265a9~tplv-k3u1fbpfcp-watermark.image) 如上圖所示,程式碼在執行的過程中建立了三個`執行上下文`:一個`全域性執行上下文`,兩個`函式執行上下文`。因為`全域性環境`只有一個,因此在程式碼的執行過程中只會建立一個`全域性執行上下文`;而`函式`可以定義多個,所以根據程式碼有可能會建立多個`函式執行上下文`。 同時`JavaScript`還會建立一個`執行上下文棧`用來管理程式碼執行過程中建立的多個`執行上下文`。 > `執行上下文棧`也可以叫做`環境棧`,在後續的描述中統一簡稱為`執行棧`。 `執行棧`和`資料結構`中的`棧`是同一種`資料型別`,有著`先進後出`的特性。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90a0347b1ce14a5faee1400d1b68b374~tplv-k3u1fbpfcp-watermark.image) # 執行上下文的建立 前面我們簡單理解了`執行上下文`的概念,同時知道了多個執行上下文是通過`執行棧`進行管理的。那`執行上下文`如何記錄當前程式碼可訪問的`變數`和`函式`將是我們接下來需要討論的問題。 首先我們需要明確`執行上下文`的`生命週期`包含兩個階段:`建立階段`和`執行階段`。 `建立階段`對應到我們的程式碼,也就是程式碼剛進入`全域性環境`或者`函式`剛被呼叫;而`執行階段`則對應程式碼一行一行在被執行。 ## 建立階段 `執行上下文`的`建立階段`會做三件事: 1. 建立`變數物件(Variable Object,簡稱VO)` 2. 建立`作用域鏈(Scope Chain)` 3. 確定`this`指向 `this`想必大家都知道,那`變數物件`和`作用域鏈`又是什麼呢,這裡先給大家梳理出這兩個的概念。 `變數物件`: 變數物件儲存著當前環境可以訪問的`變數`和`函式`,儲存方式為`key:value`,其中`key`為變數名或者函式名,`value`為變數的值或者函式引用。 `作用域鏈`:`作用域鏈`是由`變數物件`組成的一個`列表`或者`連結串列`結構,`作用域鏈`的最前端是當前環境的`變數物件`,`作用域`的下一個元素是上一個`環境`的`變數物件`,再下一個元素是上上一個環境的`變數物件`,一直到全域性的環境中的`變數物件`;`全域性環境`的`變數物件`始終是`作用域鏈`的最後一個物件。當我們在一段程式碼中訪問某個`變數`或者`函式`時,會在當前環境的執行上下文的變數物件中查詢`變數`或者`函式`,如果沒有找到,則會沿著`作用域鏈`一直向下查詢`變數`和`函式`。 > 這裡的描述的`環境`無非兩種,一種是全域性的環境,一種是函式所在的環境。 > 此處參考`《JavaScript高階程式設計》`第三版第4章2節。 相信很多人此刻已經沒有信心在往下看了,因為我已經丟擲了好多的概念:`執行上下文`、`執行上下文棧`、`變數物件`、`作用域鏈`等等。不過沒有關係,我們不用太過於糾結這些所謂的名詞,以上的內容大致有個印象即可,繼續往下看,疑惑會慢慢解開。 #### 全域性執行上下文 我們先以`全域性環境`為例,分析一下`全域性執行上下文`的`建立階段`會有怎樣的行為。 前面我們說過`全域性執行上下文`的`建立階段`對應程式碼剛進入`全域性環境`,這裡為了模擬程式碼剛進入`全域性環境`,我在`JavaScript`指令碼最開始的地方打了`斷點`。 ```javascript ``` > 這種除錯方式可能不是很準確,但是可以很好的幫助我們理解抽象的概念。 執行這段程式碼,程式碼執行到`斷點`處會停下來。此時我們在`瀏覽器`的`console`工具中訪問我們定義的`變數`和`函式`。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e8755b9806004a9ebb872d83e614723d~tplv-k3u1fbpfcp-watermark.image) 可以看到,我們已經能訪問到`var`定義的`變數`,這個叫`變數宣告提升`,但是因為程式碼還未被執行,所以變數的值還是`undefined`;同時宣告的`函式`也可以正常被呼叫,這個叫為`函式宣告提升`。 前面我們說`變數物件`儲存著當前環境可以訪問到的`變數`和`函式`,所以此時`變數物件`的內容大致如下: ```javascript // 變數物件 VO:{ a: undefined, b: undefined, fn1: , // 已經是函式本身 可以呼叫 fn2: // 已經是函式本身 可以呼叫 }, ``` 此時的`this`也已經指向`window`物件。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2dc93a15f1a1415cafbe6a07bf952338~tplv-k3u1fbpfcp-watermark.image) 所以`this`內容如下: ```javascript //this儲存的是window物件的地址,即this指向window this: ``` 最後就是`作用域鏈`,在瀏覽器的斷點除錯工具中,我們可以看到`作用域鏈`的內容。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07d2ec195ab248fe953d2507d31bdc53~tplv-k3u1fbpfcp-watermark.image) 展開`Scope`項,可以看到當前的`作用域鏈`只有一個`GLobal`元素,`Global`右側還有一個`window`標識,這個表示`Global`元素的指向是`window`物件。 ```javascript // 作用域鏈 scopeChain: [Global], // 當前作用域鏈只有一個元素 ``` 到這裡,`全域性執行上下文`在`建立階段`中的`變數物件`、`作用域鏈`和`this指向`梳理如下: ```javascript // 全域性執行上下文 GlobalExecutionContext = { VO:{ a: undefined, b: undefined, fn1: , // 已經是函式本身 可以呼叫 fn2: // 已經是函式本身 可以呼叫 }, scopeChain: [Global], // 全域性環境中作用域鏈只有一個元素,就是Global,並且指向window物件 this: // this儲存的是window物件的地址,即this指向window } ``` 前面我們說`作用域鏈`是由`變數物件`組成的,`作用域鏈`的最前端是當前環境的`變數物件`。那根據這個概念,我們應該能推理出來:`GlobalExecutionContext.VO == Global == window`的結果為`true`,因為`GlobalExecutionContext.VO`和`Global`都是我們虛擬碼中定義的`變數`,在實際的程式碼中並不存在,而且我們也訪問不到真正的`變數物件`,所以還是來看看瀏覽器中的斷點除錯工具。 我們展開`Global`選項。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/09953be14ad541fbb3ed775d3476fcfd~tplv-k3u1fbpfcp-watermark.image) ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9e468fffa80341c6b2cc5a6f6930739c~tplv-k3u1fbpfcp-watermark.image) ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/85c9d4f029874b17a576ac0b256161c3~tplv-k3u1fbpfcp-watermark.image) 可以看到`Global`中是有我們定義的變數`a`、`b`和函式`fn1`、`fn2`。同時還有我們經常會用到的變數`document`函式`alert`、`conform`等,所以我們會說`Global是指向window`物件的,這裡也就能跟瀏覽器的顯示對上了。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/161feb0136fd490dba0df7f49e9c116e~tplv-k3u1fbpfcp-watermark.image) 最後就是對應的`執行棧`: ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] ``` #### 函式執行上下文 此處參考`全域性上下文`,在`fn1`函式執行前打上`斷點`。 ```javascript ``` 開啟瀏覽器,程式碼執行到`斷點`處暫停,繼續在`console`工具中訪問一些相關的`變數`和`函式`。 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d6012f3cb93843058917ebc0dbdf1774~tplv-k3u1fbpfcp-watermark.image) 根據實際的除錯結果,`函式執行上下文`的`變數物件`如下: > 其實在`函式執行山下文`中,`變數物件`不叫`變數物件`,而是被稱之為`活動物件(Active Object,簡稱AO)`,它們其實也只是叫法上的區別,所以後面的虛擬碼中,我統一寫成`VO`。 > 但是這裡有必要給大家做一個說明,以免造成一些誤解。 ```javascript // 變數物件 VO: { param1: 10, param2: 5, result: undefined, inner: , arguments:{ 0: 10, 1:5, length: 2, callee: } } ``` 對比`全域性的執行上下文`,`函式執行上下文`的`變數物件`除了函式內部定義的`變數`和`函式`,還有函式的`引數`,同時還有一個`arguments`物件。 > `arguments`物件是`所有(非箭頭)函式`中的`區域性變數`,它和函式的引數有著一定的對應關係,可以使用從`arguments`中獲得函式的引數。 `函式執行上下文`的`作用域鏈`如下: ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b734171beabe4d7ea1751223042659fe~tplv-k3u1fbpfcp-watermark.image) 用程式碼表示: ```javascript // 作用域鏈 scopeChain: [ Local, // fn1函式執行上下文的變數物件,即Fn1ExecutionContext.VO Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ] ``` `作用域鏈`最前端的元素是`Local`,也就是`當前環境`(`當前環境`就是`fn1`函式)的`變數物件`。我們可以展開`Local`,其內容基本和前面我們總結的變數物件`VO`一致。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/22437e3efdf44ac48971a8c04bfb285c~tplv-k3u1fbpfcp-watermark.image) > 這個`Local`展開的內容和前面總結的`活動物件AO`基本一致,這裡只是`Chrome`瀏覽器的展示方式,不用過多糾結。 `this`物件同樣指向了`window`。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0668a9f613d465fae8a8d481ae6ba3f~tplv-k3u1fbpfcp-watermark.image) > fn1函式內部的this指向window物件,源於`fn1`函式的呼叫方式。 總結`函式執行上下文`在`建立階段`的行為: ```javascript // 函式執行上下文 Fn1ExecutionContext = { VO: { param1: 10, param2: 5, result: undefined, inner: , arguments:{ 0: 10, 1:5, length: 2, callee: } }, scopeChain: [ Local, // fn1函式執行上下文的變數物件,即Fn1ExecutionContext.VO Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` 此時的`執行棧`如下: ```javascript // 執行棧 ExecutionStack = [ Fn1ExecutionContext, // fn1執行上下文 GlobalExecutionContext // 全域性執行上下文 ] ``` ## 執行階段 `執行上下文`的`執行階段`,相對來說比較簡單,基本上就是為變數賦值和執行每一行程式碼。這裡以`全域性執行上下文`為例,梳理執行上下文`執行階段`的行為: ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b8a94b72389748bdbb76d50e47113dca~tplv-k3u1fbpfcp-watermark.image) ```javascript // 函式執行上下文 Fn1ExecutionContext = { VO: { param1: 10, param2: 5, result: 15, inner: , arguments:{ 0: 10, 1:5, length: 2, callee: } }, scopeChain: [ Local, // fn1函式執行上下文的變數物件,即Fn1ExecutionContext.VO Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` # 執行上下文的擴充套件 堅持看到這裡的同學,相信大家對`JavaScript`的執行上下文已經有了一點的認識。那前面為了讓大家更好的理解`JavaScript`的執行上下文,我省略了一些特殊的情況,那接下來緩口氣,我們在來看看有關`執行上下文`的更多內容。 ## let和const 對`ES6`特性熟悉的同學都知道`ES6`新增了兩個定義變數的`關鍵字`:`let`和`const`,並且這兩個關鍵字不存在`變數宣告提升`。 還是前面的一系列除錯方法,我們分析一下`全域性環境`中的`let`和`const`。首先我們執行下面這段`JavaScript`程式碼。 ```javascript ``` 斷點處訪問變數`a`和`b`,發現出現了錯誤。 ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e0160cfe5b94c8b89378b29d803db0d~tplv-k3u1fbpfcp-watermark.image) 那這個說明在`執行上下文`的`執行階段`,我們是無法訪問`let`、`const`定義的變數,即進一步證實了`let`和`const`不存在`變數宣告提升`。也說明了在`執行上下文`的`建立階段`,`變數物件`中沒有`let`、`const`定義的變數。 ## 函式 函式一般有兩種定義方式,第一種是`函式宣告`,第二種是`函式表示式`。 ```javascript // 函式宣告 function fn1(){ // do something } // 函式表示式 var fn2 = function(){ // do something } ``` 接著我們來執行下面的這段程式碼。 ```javascript ``` 程式碼執行到斷點處暫停,手動呼叫函式:`fn1`和`fn2`。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/38c52dec3df14553b5b94727c46bbd96~tplv-k3u1fbpfcp-watermark.image) 從結果可以看到,對於`函式宣告`,因為存在`函式宣告提升`,所以可以在函式定義前使用函式;而對於`函式表示式`,在函式定義前使用會導致錯誤,說明`函式表示式`不存在`函式宣告提升`。 這個例子補充了前面的內容:在`執行上下文`的`建立階段`,`變數物件`的內容不包含`函式表示式`。 ## 詞法環境 在梳理這篇文章的過程中,看到很多文章提及到了`詞法環境`和`變數環境`這個概念,那這個概念是`ES5`提出來的,是前面我們所描述的`變數物件`和`作用域鏈`的另一種設計和實現。基於`ES5`新提出來這個概念,對應的`執行上下文`表示也會發生變化。 ```javascript // 執行上下文 ExecutionContext = { // 詞法環境 LexicalEnvironment: { // 環境記錄 EnvironmentRecord: { }, // 外部環境引用 outer: }, // 變數環境 VariableEnvironment: { // 環境記錄 EnvironmentRecord: { }, // 外部環境引用 outer: }, // this指向 this: } ``` `詞法環境`由`環境記錄`和`外部環境引用`組成,其中`環境記錄`和`變數物件`類似,儲存著當前`執行上下文`中的`變數`和`函式`;同時`環境記錄`在全域性執行上下文中稱為`物件環境記錄`,在函式執行上下文中稱為`宣告性環境記錄`。 ```javascript // 全域性執行上下文 GlobalExecutionContext = { // 詞法環境 LexicalEnvironment: { // 環境記錄之物件環境記錄 EnvironmentRecord: { Type: "Object" // type標識,表明該環境記錄是物件環境記錄 }, // 外部環境引用 outer: } } // 函式執行上下文 FunctionExecutionContext = { // 詞法環境 LexicalEnvironment: { // 環境記錄之宣告性環境記錄 EnvironmentRecord: { Type: 'Declarative' // type標識,表明該環境記錄是宣告性環境記錄 }, // 外部環境引用 outer: } } ``` 這點就類似`變數物件`也只存在於`全域性上下文中`,而在`函式上下文中`稱為`活動物件`。 `詞法環境`中的`外部環境`儲存著其他執行上下文的`詞法環境`,這個就類似於`作用域鏈`。 除了`詞法環境`之外,還有一個`名詞`叫`變數環境`,它實際也是`詞法環境`,這兩者的區別是`變數環境`只儲存用`var`宣告的變數,除此之外像`let`、`const`定義的`變數`、`函式宣告`、函式中的`arguments`物件等,均儲存在`詞法環境中`。 以這段程式碼為例: ```javascript var a = 10; var b = 5; let m = 10; function fn1(param1, param2){ var result = param1 + param2; function inner() { return 'inner go'; } inner(); return 'fn1 go' } fn1(a,b); ``` 如果以`ES5`中新提及的`詞法環境`和`變數環境`概念來表示`執行上下文`,應該是下面這樣: ```javascript // 執行棧 ExecutionStack = [ fn1ExecutionContext, // fn1執行上下文 GlobalExecutionContext, // 全域性執行上下文 ] // fn1執行上下文 fn1ExecutionContext = { // 詞法環境 LexicalEnvironment: { // 環境記錄 EnvironmentRecord: { Type: 'Declarative', // 函式的環境記錄稱之為宣告性環境記錄 arguments: { 0: 10, 1: 5, length: 2 }, inner: }, // 外部環境引用 outer: }, // 變數環境 VariableEnvironment: { // 環境記錄 EnvironmentRecord: { Type: 'Declarative', // 函式的環境記錄稱之為宣告性環境記錄 result: undefined, // 變數環境只儲存var宣告的變數 }, // 外部環境引用 outer: } } // 全域性執行上下文 GlobalExecutionContext = { // 詞法環境 LexicalEnvironment: { // 環境記錄 EnvironmentRecord: { Type: 'Object', // 全域性執行上下文的環境記錄稱為物件環境記錄 m: < uninitialized >, fn1: , fn2: }, // 外部環境引用 outer: // 全域性執行上下文的外部環境引用為null }, // 變數環境 VariableEnvironment: { // 環境記錄 EnvironmentRecord: { Type: 'Object', // 全域性執行上下文的環境記錄稱為物件環境記錄 a: undefined, // 變數環境只儲存var宣告的變數 b: undefined, // 變數環境只儲存var宣告的變數 }, // 外部環境引用 outer: // 全域性執行上下文的外部引用為null } } ``` 以上的內容基本上參考這篇文章:[【譯】理解 Javascript 執行上下文和執行棧](https://juejin.cn/post/6844903704466833421)。關於`詞法環境`相關的內容沒有過多研究,所以本篇文章就不在多講,後面的一些內容還是會以`變數物件`和`作用域鏈`為準。 # 除錯方法說明 關於本篇文章中的除錯方法,僅僅是我自己實踐的一種方式,比如在`斷點`處程式碼暫停執行,然後我在`console`工具中訪問`變數`或者`呼叫函式`,其實大可以將這些寫入程式碼中。 ```javascript console.log(a); fn1(); fn2(); var a = 10; function fn1(){ return 'fn1 go'; } var fn2 = function (){ return 'fn2 go'; } ``` 在程式碼未執行到`變數宣告`和`函式宣告`處,都可以暫且認為處於`執行上下文`的`建立階段`,當變數訪問出錯或者函式調用出錯,也可以得出同樣的結論,而且這種方式也非常的準確。 反而是我這種除錯方法的實踐過程中,會出現很多和實際不符的現象,比如下面這個例子。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc5cbf1cc1bc49f7815c0961e7faa4f5~tplv-k3u1fbpfcp-watermark.image) 前面我們其實給出過正確結論:`函式宣告`,可以在函式定義前使用函式,而函式表示式不可以。而如果是我這種除錯方式,會發現此時呼叫`inner`和`other`都會出錯。 其原因我個人猜測應該是瀏覽器`console`工具的上層實現的原因,如果你也遇到同樣的問題,不必過分糾結,一定要將實際的程式碼執行結果和書中的理論概念結合起來,正確的理解`JavaScript`的`執行上下文`。 # 躬行實踐 臺下十年功,終於到了臺上的一分鐘了。瞭解了`JavaScript`的`執行上下文`之後,對於網上流傳的一些高頻面試題和程式碼,都可以用`執行上下文`中的相關知識來分析。 首先是本文開篇貼出的兩段程式碼。 ## 程式碼一 ```javascript console.log(a); var a = 10; ``` 這段程式碼的執行結果相信大家已經瞭然於胸:`console.log`的結果是`undefined`。其原理也很簡單,就是`變數宣告提升`。 ## 程式碼二 ```javascript fn1(); fn2(); function fn1(){ console.log('fn1'); } var fn2 = function(){ console.log('fn2'); } ``` 這個示例應該也是小菜一碟,前面我們已經做過程式碼除錯:`fn1`可以正常呼叫,呼叫`fn2`會導致`ReferenceError`。 ## 程式碼三 ```javascript var numberArr = []; for(var i = 0; i<5; i++){ numberArr[i] = function(){ return i; } } numberArr[0](); numberArr[1](); numberArr[2](); numberArr[3](); numberArr[4](); ``` 此段程式碼如果刷過面試題的同學一定知道答案,那這次我們用`執行上下文`的知識點對其進行分析。 #### step 1 程式碼進入`全域性環境`,開始`全域性執行上下文`的`建立階段`: ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] // 全域性執行上下文 GlobalExecutionContext = { VO: { numberArr: undefined, i: undefined, }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` #### step 2 接著程式碼一行一行被執行,開始`全域性執行上下文`的`執行階段`。 當代碼開始進入第一個迴圈: ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] // 全域性執行上下文 GlobalExecutionContext = { VO: { // 這種寫法代表number是一個Array型別,長度為1,第一個元素是一個Function numberArr: Array[1][f()], i: 0, }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` > 上面總結的`執行上下文`內容是程式碼已經進入到第一個迴圈,跳過了`numberArr`的`宣告`和`賦值`,後面所有的程式碼只分析`關鍵部分`,不會一行一行的分析。 #### step 3 程式碼進入第五次迴圈(第五次迴圈因為不滿足條件並不會真正執行,但是`i`值已經加`1`): > 省略`i=2`、`i = 3`和`i = 4`的執行上下文內容。 ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] // 全域性執行上下文 GlobalExecutionContext = { VO: { // 這種寫法代表number是一個Array型別,長度為5,元素均為Function numberArr: Array[5][f(), f(), f(), f(), f()], i: 5, }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` `迴圈`部分結束以後,我們發現`i`此時的值已經是`5`了。 #### step 4 接著我們訪問`numberArr`中的`元素`(`numberArr`中的每一個元素都是一個`匿名函式`,函式返回`i`的值)並呼叫。首先是訪問下標為`0`的元素,之後呼叫對應的`匿名函式`,既然是`函式呼叫`,說明還會生成一個`函式執行上下文`。 ```javascript // 執行棧 ExecutionStack = [ FunctionExecutionContext // 匿名函式執行上下文 GlobalExecutionContext // 全域性執行上下文 ] // 匿名函式執行上下文 FunctionExecutionContext = { VO: {}, // 變數物件為空 scopeChain: [ LocaL, // 匿名函式執行上下文的變數物件,即FunctionExecutionContext.VO Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: // this指向numberArr this == numberArr 值為true } // 全域性執行上下文 GlobalExecutionContext = { VO: { // 這種寫法代表number是一個Array型別,長度為5,元素均為Function numberArr: Array[5][f(), f(), f(), f(), f()], i: 5, }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` 呼叫`匿名函式`時,`函式執行上下文`的`變數物件`的值為空,所以當該`匿名函式`返回`i`時,在自己的`變數物件`中沒有找到對應的`i`值,就會沿著自己的`作用域鏈(scopeChain)`去全域性執行上下文的變數物件`Global`中查詢,於是返回了`5`。 那後面訪問`numberArr`變數的`第1個`、`第2個`、`...`、`第4個`元素也是同樣的道理,均會返回`5`。 ## 程式碼四 ```javascript var numberArr = []; for(let i = 0; i<5; i++){ numberArr[i] = function(){ return i; } } console.log(numberArr[0]()); console.log(numberArr[1]()); console.log(numberArr[2]()); console.log(numberArr[3]()); console.log(numberArr[4]()); ``` 這段程式碼和上面一段程式碼基本一致,只是我們將迴圈中控制次數的變數`i`使用了`let`關鍵字宣告,那接下來開始我們的分析。 #### step 1 首先是`全域性執行上下文`的`建立階段`: ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] // 全域性執行上下文 GlobalExecutionContext = { VO: { numberArr: undefined }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` 因為`let`關鍵字不存在`變數提升`,因此`全域性執行上下文`的`變數物件`中並沒有變數`i`。 #### step 2 當代碼一行一行的執行,開始`全域性執行上下文`的`執行階段`。 以下是程式碼執行進入第一次迴圈: ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] // 全域性執行上下文 GlobalExecutionContext = { VO: { // 這種寫法代表number是一個Array型別,長度為1,第一個元素是一個Function numberArr: Array[1][f()], }, scopeChain: [ Block, // let定義的for迴圈形成了一個塊級作用域 Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` 可以看到當迴圈開始執行時,因為遇到了`let`關鍵字,因此會建立一個`塊級作用域`,裡面包含了變數`i`的值。這個`塊級作用域`非常的關鍵,正是因為這個`塊級作用域`在迴圈的時候儲存了變數的值,才使得這段程式碼的執行結果不同於上一段程式碼。 ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1f2af3c442d45e682ec3859fb0fbefc~tplv-k3u1fbpfcp-watermark.image) #### step 3 `i`值為`5`時: > 省略`i=1`、`i = 3`和`i = 4`的執行上下文內容。 ```javascript GlobalExecutionContext = { VO: { // 這種寫法代表number是一個Array型別,長度為2,元素均為Function numberArr: Array[5][f(), f(), f(), f(), f()], }, scopeChain: [ Block, Global ], this: } ``` 此時`塊級作用域`中變數`i`的值也同步更新為`5`。 ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/824274ac74c0419eb2b83fbed2a9f64c~tplv-k3u1fbpfcp-watermark.image) #### step 4 接著就是訪問陣列中的第一個元素,呼叫`匿名函式`,`匿名函式`在執行的時候會建立一個`函式執行上下文`。 ```javascript // 執行棧 ExecutionStack = [ FunctionExecutionContext, // 匿名函式執行上下文 GlobalExecutionContext // 全域性執行上下文 ] // 匿名函式執行上下文 FunctionExecutionContext = { VO: {}, // 變數物件為空 scopeChain: [ LocaL, // 匿名函式執行上下文的變數物件,即FunctionExecutionContext.VO Block, // 塊級作用域 Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: // this指向numberArr this == numberArr 值為true } // 全域性執行上下文 GlobalExecutionContext = { VO: { // 這種寫法代表number是一個Array型別,長度為2,元素均為Function numberArr: Array[5][f(), f(), f(), f(), f()], }, scopeChain: [ Global ], this: } ``` 該`匿名函式`因為儲存著`let`關鍵字定義的變數`i`,因此`作用域鏈`中會儲存著`第一次迴圈`時建立的那個`塊級作用域`,這個`塊級作用域`前面我們說過也在瀏覽器的除錯工具中看到過,它儲存著當前迴圈的`i`值。 所以當`return i`時,當前執行上下文的變數物件為空,就沿著作用域向下查詢,在`Block`中找到對應的變數`i`,因此返回`0`;後面訪問`numberArr[1]()`、`numberArr[2]()`、...、`numberArr[4]()`也是同樣的道理。 ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/80690081494e4bf6aa4f691f9b202cc8~tplv-k3u1fbpfcp-watermark.image) ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/be06057dea964ce4a966e589b62a2631~tplv-k3u1fbpfcp-watermark.image) ## 程式碼五 ```jvascript var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); ``` 這段程式碼包括下面的都是在梳理這篇文章的過程中,看到的一個很有意思的示例,所以貼在這裡和大家一起分析一下。 #### step 1 程式碼進入`全域性環境`,開始`全域性執行上下文`的`建立階段`: ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: undefined, checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` #### step 2 `全域性執行上下文`的`執行階段`: ```javascript // 執行棧 ExecutionStack = [ GlobalExecutionContext // 全域性執行上下文 ] // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', // 變數賦值 checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` #### step 3 當代碼執行到最後一行:`checkscope()`,開始`checkscope函式執行上下文`的`建立階段`。 ```javascript // 執行棧 ExecutionStack = [ CheckScopeExecutionContext, // checkscope函式執行上下文 GlobalExecutionContext // 全域性執行上下文 ] // 函式執行上下文 CheckScopeExecutionContext = { VO: { scope: undefined, f: , // 函式已經可以被呼叫 }, scope: [ Local, // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO Global //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO ], this: } // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` #### step 4 接著是`checkscope函式執行上下文`的`執行階段`: ```javascript // 執行棧 ExecutionStack = [ CheckScopeExecutionContext, // 函式執行上下文 GlobalExecutionContext // 全域性執行上下文 ] // 函式執行上下文 CheckScopeExecutionContext = { VO: { scope: 'local scope', // 變數賦值 f: , // 函式已經可以被呼叫 }, scope: [ Local, // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO Global //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO ], this: } // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` #### step 5 執行到`return f()`時,進入`f函式執行上下文`的`建立階段`: ```javascript // 函式執行上下文的建立階段 FExecutionContext = { VO: {}, scope: [ Local, // f執行上下文的變數物件 也就是FExecutionContext.VO Local, // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO Global //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO ], this: } // 函式執行上下文 CheckScopeExecutionContext = { VO: { scope: 'local scope', f: , // 函式已經可以被呼叫 }, scope: [ Local, // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO Global //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO ], this: } // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` 當`f函式`返回`scope`變數時,當前`f執行上下文中`的`變數物件`中沒有名為`scope`的變數,所以沿著`作用域鏈`向上查詢,發現`checkscope`執行上下文的變數物件`Local`中包含`scope`變數,所以返回`local scope`。 ## 程式碼六 ``` var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); ``` 這段程式碼和上面的程式碼非常的相似,只不過`checkscope`函式的返回值沒有直接呼叫`f`函式,而是將`f`函式返回,在`全域性環境`中呼叫了`f`函式。 #### step 1 `全域性執行上下文`的`建立階段`: ```javascript // 執行棧 ExcutionStack = [ GlobalExcutionContext ]; // 全域性執行上下文的建立階段 GlobalExecutionContext = { VO: { scope: undefined, checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` #### step 2 `全域性執行上下文`的`執行階段`: ```javascript // 執行棧 ExcutionStack = [ GlobalExcutionContext // 全域性執行上下文 ]; // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', // 變數賦值 checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件,即GlobalExecutionContext.VO ], this: } ``` #### step 3 當代碼執行到最後一行:`checkscope()()`,先執行`checkscope()`,也就是開始`checkscope函式執行上下文`的`建立階段`。 ```javascript // 執行棧 ExcutionStack = [ CheckScopeExecutionContext, // checkscope函式執行上下文 GlobalExcutionContext // 全域性執行上下文 ] // checkscope函式執行上下文的建立階段 CheckScopeExecutionContext = { VO: { scope: undefined, f: , // 函式已經可以被呼叫 }, scopeChain: [ Local, // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO Global //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO ], this: } // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: , // 函式已經可以被呼叫 }, scopeChain: [Global], this: } ``` #### step 4 接著是`checkscope函式執行上下文`的`執行階段`: ```javascript // 執行棧 ExcutionStack = [ CheckScopeExecutionContext, // checkscope函式執行上下文 GlobalExcutionContext // 全域性執行上下文 ] // checkscope函式執行上下文 CheckScopeExecutionContext = { VO: { scope: 'local scope', f: , // 函式已經可以被呼叫 }, scopeChain: [ Local, // checkscope執行上下文的變數物件 也就是CheckScopeExecutionContext.VO Global //全域性執行上下文的變數物件 也就是GlobalExecutionContext.VO ], this: } // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件 ], this: } ``` #### step 5 執行到`return f`時,此處並不同上一段程式碼,並沒有呼叫`f`函式,所以不會建立`f`函式的執行上下文,因此直接將`函式f`返回,此時`checkscope`函式執行完畢,會從`執行棧`中彈出`checkscope`的`執行山下文`。 ```javascript // 執行棧 (此時CheckScopeExecutionContext已經從棧頂被彈出) ExcutionStack = [ GlobalExecutionContext // 全域性執行上下文 ]; // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: , // 函式已經可以被呼叫 }, scopeChain: [ Global // 全域性執行上下文的變數物件 ], this: } ``` #### step 6 在`step3`中,`checkscope()()`程式碼的前半部分執行完畢,返回`f函式`;接著執行後半部分`()`,也就是呼叫`f函式`。那此時進入`f函式執行上下文`的`建立階段`: ```javascript // 執行棧 ExcutionStack = [ fExecutionContext, // f函式執行上下文 GlobalExecutionContext // 全域性執行上下文 ]; // f函式執行上下文 fExecutionContext = { VO: {}, // f函式的變數物件為空 scopeChain: [ Local, // f函式執行上下文的變數物件 Local, // checkscope函式執行上下文的變數物件 Global, // 全域性執行上下文的變數物件 ], this: } // 全域性執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: , // 函式已經可以被呼叫 }, scopeChain: [Global], this: } ``` 我們看到在`f`函式執行上下文的`建立階段`,其`變數物件`為空字典,而其`作用域鏈`中卻儲存這`checkscope執行上下文`的`變數物件`,所以當代碼執行到`return scope`時,在`f`函式的`變數物件`中沒找到`scope`變數,便沿著作用域鏈,在`chckscope`執行上下文的變數物件`Local`中找到了`scope`變數,所以返回`local scope`。 # 總結 相信很多人和我一樣,在剛開始學習和理解`執行山下文`的時候,會因為概念過於抽象在加上沒有合適的實踐方式,對`JavaScript`的執行上下文百思不解。作者也是花了很久的時間,閱讀很多相關的書籍和文章,在加上一些實踐才梳理出來這篇文章,希望能給大家一些幫助,如果文中描述有誤,還希望不吝賜教,提出寶貴的意見和建議。 # 文末 如果這篇文章有幫助到你,❤️關注+點贊+收藏+評論+轉發❤️鼓勵一下作者 文章`公眾號`首發,關注`不知名寶藏女孩`第一時間獲取最新的文章 筆芯❤️~ ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/56fbe8abec3f4982a51fc293d2d66851~tplv-k3u1fbpfcp-watermark.image)