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