做者:小土豆
博客園:https://www.cnblogs.com/HouJiao/
掘金:https://juejin.im/user/2436173500265335javascript
在正文開始前,先來看兩個JavaScript
代碼片斷。前端
console.log(a); var a = 10;
fn1(); fn2(); function fn1(){ console.log('fn1'); } var fn2 = function(){ console.log('fn2'); }
若是你能正確的回答
並解釋
以上代碼的輸出結果
,那說明你對JavaScript
的執行上下文
已經有必定的瞭解;反之,閱讀完這篇文章,相信你必定會獲得答案。java
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
還會建立一個執行上下文棧
用來管理代碼執行過程當中建立的多個執行上下文
。函數
執行上下文棧
也能夠叫作環境棧
,在後續的描述中統一簡稱爲執行棧
。工具
執行棧
和數據結構
中的棧
是同一種數據類型
,有着先進後出
的特性。post
前面咱們簡單理解了執行上下文
的概念,同時知道了多個執行上下文是經過執行棧
進行管理的。那執行上下文
如何記錄當前代碼可訪問的變量
和函數
將是咱們接下來須要討論的問題。
首先咱們須要明確執行上下文
的生命週期
包含兩個階段:建立階段
和執行階段
。
建立階段
對應到咱們的代碼,也就是代碼剛進入全局環境
或者函數
剛被調用;而執行階段
則對應代碼一行一行在被執行。
執行上下文
的建立階段
會作三件事:
變量對象(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
的執行上下文,我省略了一些特殊的狀況,那接下來緩口氣,咱們在來看看有關執行上下文
的更多內容。
對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]();
此段代碼若是刷過面試題的同窗必定知道答案,那此次咱們用執行上下文
的知識點對其進行分析。
代碼進入全局環境
,開始全局執行上下文
的建立階段
:
// 執行棧 ExecutionStack = [ GlobalExecutionContext // 全局執行上下文 ] // 全局執行上下文 GlobalExecutionContext = { VO: { numberArr: undefined, i: undefined, }, scopeChain: [ Global<window> // 全局執行上下文的變量對象,即GlobalExecutionContext.VO ], this: <window reference> }
接着代碼一行一行被執行,開始全局執行上下文
的執行階段
。
當代碼開始進入第一個循環:
// 執行棧 ExecutionStack = [ GlobalExecutionContext // 全局執行上下文 ] // 全局執行上下文 GlobalExecutionContext = { VO: { // 這種寫法表明number是一個Array類型,長度爲1,第一個元素是一個Function numberArr: Array[1][f()], i: 0, }, scopeChain: [ Global<window> // 全局執行上下文的變量對象,即GlobalExecutionContext.VO ], this: <window reference> }
上面總結的
執行上下文
內容是代碼已經進入到第一個循環,跳過了numberArr
的聲明
和賦值
,後面全部的代碼只分析關鍵部分
,不會一行一行的分析。
代碼進入第五次循環(第五次循環由於不知足條件並不會真正執行,可是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
了。
接着咱們訪問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
關鍵字聲明,那接下來開始咱們的分析。
首先是全局執行上下文
的建立階段
:
// 執行棧 ExecutionStack = [ GlobalExecutionContext // 全局執行上下文 ] // 全局執行上下文 GlobalExecutionContext = { VO: { numberArr: undefined }, scopeChain: [ Global<window> // 全局執行上下文的變量對象,即GlobalExecutionContext.VO ], this: <window reference> }
由於let
關鍵字不存在變量提高
,所以全局執行上下文
的變量對象
中並無變量i
。
當代碼一行一行的執行,開始全局執行上下文
的執行階段
。
如下是代碼執行進入第一次循環:
// 執行棧 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
的值。這個塊級做用域
很是的關鍵,正是由於這個塊級做用域
在循環的時候保存了變量的值,才使得這段代碼的運行結果不一樣於上一段代碼。
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
。
接着就是訪問數組中的第一個元素,調用匿名函數
,匿名函數
在執行的時候會建立一個函數執行上下文
。
// 執行棧 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();
這段代碼包括下面的都是在梳理這篇文章的過程當中,看到的一個頗有意思的示例,因此貼在這裏和你們一塊兒分析一下。
代碼進入全局環境
,開始全局執行上下文
的建立階段
:
// 執行棧 ExecutionStack = [ GlobalExecutionContext // 全局執行上下文 ] // 全局執行上下文 GlobalExecutionContext = { VO: { scope: undefined, checkscope: <Function checkscope>, // 函數已經能夠被調用 }, scopeChain: [ Global<window> // 全局執行上下文的變量對象,即GlobalExecutionContext.VO ], this: <window reference> }
全局執行上下文
的執行階段
:
// 執行棧 ExecutionStack = [ GlobalExecutionContext // 全局執行上下文 ] // 全局執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', // 變量賦值 checkscope: <Function checkscope>, // 函數已經能夠被調用 }, scopeChain: [ Global<window> // 全局執行上下文的變量對象,即GlobalExecutionContext.VO ], this: <window reference> }
當代碼執行到最後一行: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> }
接着是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> }
執行到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
函數。
全局執行上下文
的建立階段
:
// 執行棧 ExcutionStack = [ GlobalExcutionContext ]; // 全局執行上下文的建立階段 GlobalExecutionContext = { VO: { scope: undefined, checkscope: <Function checkscope>, // 函數已經能夠被調用 }, scopeChain: [ Global<window> // 全局執行上下文的變量對象,即GlobalExecutionContext.VO ], this: <window reference> }
全局執行上下文
的執行階段
:
// 執行棧 ExcutionStack = [ GlobalExcutionContext // 全局執行上下文 ]; // 全局執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', // 變量賦值 checkscope: <Function checkscope>, // 函數已經能夠被調用 }, scopeChain: [ Global<window> // 全局執行上下文的變量對象,即GlobalExecutionContext.VO ], this: <window reference> }
當代碼執行到最後一行: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> }
接着是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> }
執行到return f
時,此處並不一樣上一段代碼,並無調用f
函數,因此不會建立f
函數的執行上下文,所以直接將函數f
返回,此時checkscope
函數執行完畢,會從執行棧
中彈出checkscope
的執行山下文
。
// 執行棧 (此時CheckScopeExecutionContext已經從棧頂被彈出) ExcutionStack = [ GlobalExecutionContext // 全局執行上下文 ]; // 全局執行上下文 GlobalExecutionContext = { VO: { scope: 'global scope', checkscope: <Function checkscope>, // 函數已經能夠被調用 }, scopeChain: [ Global<window> // 全局執行上下文的變量對象 ], this: <window reference> }
在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
的執行上下文百思不解。做者也是花了好久的時間,閱讀不少相關的書籍和文章,在加上一些實踐才梳理出來這篇文章,但願能給你們一些幫助,若是文中描述有誤,還但願不吝賜教,提出寶貴的意見和建議。
若是這篇文章有幫助到你,❤️關注+點贊+收藏+評論+轉發❤️鼓勵一下做者
文章公衆號
首發,關注不知名寶藏女孩
第一時間獲取最新的文章
筆芯❤️~