JavaScript的執行上下文,真沒你想的那麼難
阿新 • • 發佈:2020-12-01
> 作者:小土豆
> 部落格園: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)