作者:小土豆
博客園: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
的執行上下文百思不解。作者也是花了很久的時間,閱讀很多相關的書籍和文章,在加上一些實踐才梳理出來這篇文章,希望能給大家一些幫助,如果文中描述有誤,還希望不吝賜教,提出寶貴的意見和建議。
文末
如果這篇文章有幫助到你,❤️關注+點贊+收藏+評論+轉發❤️鼓勵一下作者
文章公眾號
首發,關注不知名寶藏女孩
第一時間獲取最新的文章
筆芯❤️~