首先我們知道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引擎能正確執行程式碼的順序。