首先我們知道JavaScript引擎包括一個調用棧和堆,調用棧是代碼實際執行的地方,使用執行上下文(執行環境)來完成;堆是非結構化的記憶體池,存儲了應用程式所需要的所有對象。 執行上下文是什麼? 執行上下文包括全局執行上下文和執行上下文。 全局執行上下文:代碼編譯完成後進入調用棧執行首先創建全局執行上 ...
首先我們知道JavaScript引擎包括一個調用棧和堆,調用棧是代碼實際執行的地方,使用執行上下文(執行環境)來完成;堆是非結構化的記憶體池,存儲了應用程式所需要的所有對象。
執行上下文是什麼?
執行上下文包括全局執行上下文和執行上下文。
全局執行上下文:代碼編譯完成後進入調用棧執行首先創建全局執行上下文(整個項目只有一個全局執行上下文),是用來執行頂層代碼(函數除外,函數只在被調用的時候執行)。
執行上下文:執行一段JavaScript的環境,存儲了一些代碼執行所需要的必要信息,比如傳遞給函數的局部變數或者參數。打個比方:我們點外賣,送來的袋子(執行上下文)不只有外賣(JavaScript代碼),還有餐具(代碼執行所需要的必要信息)。
每一個函數調用就會創建執行上下文來執行。
執行上下文分為三個部分,依次為變數環境,作用域和this關鍵字:
變數環境VE
-
let,const and var變數
-
函數聲明
-
函數形參
作用域
作用域(scoping):由JavaScript引擎組織和訪問,控制我們程式的變數。
主要分為以下三個
-
全局作用域:在代碼中任何地方都能訪問到的對象擁有全局作用域。
-
函數作用域:只能在函數中訪問到的對象具有函數作用域,也稱局部作用域。
-
塊作用域:ES6新特性,類似於函數作用域,指的是大括弧括起來的塊,比如if和for迴圈,塊中的變數只能在塊中訪問,具有塊作用域。但只能用let和const,使用var仍能被全局訪問。
if (birthYear >= 1981 && birthYear <= 1996) { var millenial = true; const str = `oh,you're a millenial,${firstName}`; console.log(str); function add(a, b) { return a + b; } } console.log(str);//str不能被列印 console.log(add(1, 1));//add函數不能被列印 console.log(millenial);//var變數能列印,能被全局訪問
作用域鏈:若在當前作用域無法查找到需要變數,則通過作用域鏈來進行變數查找,子作用域查找使用父作用域的變數。
註:如果子作用域和父作用域存在相同的變數名,則直接查找子作用域的,無需進行變數查找。
this關鍵字
定義:為每個執行上下文(函數)創建的特殊變數,取的值為該函數的調用者本身,具體取值包括以下四種方法 this為調用該方法的對象
-
方法 this指向調用方法的對象
const luki = { name : 'lukirence', year : 2002, calcAge:function(){ return 2037-this.year; } };
如果在方法內的函數嵌套一個新的函數,該嵌套函數相當於常規函數,this關鍵字為undefined
const luki = { fullName: 'lukirence', year: 2002, calcAge: function () { console.log(this); console.log(2037 - this.year); const isMillenial = function(){ console.log(this);//undefined console.log(this.year >=1981); } isMillenial(); } };
如何解決嵌套函數能夠使用this?
- 添加self變數=this(ES6之前的舊方法)
const luki = { fullName: 'lukirence', year: 2002, calcAge: function () { console.log(this); console.log(2037 - this.year); const self = this; const isMillenial = function(){ console.log(self); console.log(self.year >=1981); } isMillenial(); } };
- 將嵌套函數改成箭頭函數
const luki = { fullName: 'lukirence', year: 2002, calcAge: function () { console.log(this); console.log(2037 - this.year); const isMillenial = ()=>{ console.log(this); console.log(this.year >=1981); } isMillenial(); } };
-
常規函數聲明 this為undefined
const calcAge = function (birthYear) { console.log(2037 - birthYear); console.log(this);//顯示undefined }; calcAge(1991);
-
箭頭函數 箭頭函數沒有this關鍵字,箭頭函數的關鍵詞會通過查找父函數的關鍵詞,若沒有則為全局的關鍵詞,即指向全局視窗。
console.log(this);//指向全局視窗window const calcAgeArrow = birthYear => { console.log(2037 - birthYear); console.log(this);//指向全局視窗window }; calcAgeArrow(1991); const luki = { fullName: 'lukirence', year: 2002, calcAge: function () { console.log(this); console.log(2037 - this.year); }, greet: () => { console.log(`hi,${this.fullName}`); }, }; luki.greet();//顯示hi undefined,因為this指向window對象
- 事件監聽 this為事件處理器所添加的DOM元素
註:箭頭函數的執行上下文不包括參數對象和this關鍵字
執行上下文創建
在詳細瞭解了執行上下文的內容後,我們來看一段代碼實際執行時執行上下文如何在調用棧中活動的。
將以以下代碼為例,按每一步驟描述執行上下文創建流程:
const name = 'luki';//--------------------------------1
const first =() =>{//---------------------------------2
let a =1;//-------------------------------------2.1
const b =second(1,2);//-------------------------2.2
a=a+b;//----------------------------------------2.3
return a;//-------------------------------------2.4
};
function second(x,y){//-------------------------------3
var c =2;//-------------------------------------3.1
return c;//-------------------------------------3.2
}
const x =first();//-----------------------------------4
-
代碼被編譯後先創建全局執行上下文推入調用棧(call stack);
-
執行頂層代碼(序號1,2,3):運行1聲明name變數; 運行2聲明first函數;運行3聲明second函數;保存以上變數環境到全局上下文中;
-
執行到序號4開始調用first()函數,並創建first()的執行上下文推入調用棧,準備執行first()內部代碼跳轉到2.1;
-
運行到2.1聲明變數a保存到first()的執行上下文;
-
運行2.2調用新函數second(),創建second()的執行上下文推入調用棧,停止first()的執行跳轉到3.1;
-
運行3.1聲明變數c保存到second()的執行上下文;
-
運行3.2return語句表示完成該函數執行,second()的執行上下文將從調用棧中彈出(此處雖然彈出,但是其中變數環境仍可能被使用,涉及到閉包的概念),調用棧重新指向first(),代碼重新跳轉回2.3;
-
運行2.3執行代碼內容;
-
運行2.4,first()的執行上下文將從調用棧中彈出,調用棧重新指向gloal(),代碼重新跳轉回4,將返回值最終賦值給x;
- 此後調用棧一直保持在這個狀態直到我們關閉瀏覽器來終止程式,最終彈出global()全局上下文;
由此我們可以發現調用棧的執行上下文根據函數調用來出入棧確保了JS引擎能正確執行代碼的順序。