摘要:執(zhí)行上下文和執(zhí)行棧是中關(guān)鍵概念之一,是難點之一。理解執(zhí)行上下文和執(zhí)行棧同樣有助于理解其他的概念如提升機制作用域和閉包等。函數(shù)執(zhí)行完成,函數(shù)的執(zhí)行上下文出棧,并且被銷毀。
前言
如果你是一名 JavaScript 開發(fā)者,或者想要成為一名 JavaScript 開發(fā)者,那么你必須知道 JavaScript 程序內(nèi)部的執(zhí)行機制。執(zhí)行上下文和執(zhí)行棧是JavaScript中關(guān)鍵概念之一,是JavaScript難點之一。 理解執(zhí)行上下文和執(zhí)行棧同樣有助于理解其他的 JavaScript 概念如提升機制、作用域和閉包等。本文盡可能用通俗易懂的方式來介紹這些概念。
想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客
一、執(zhí)行上下文(Execution Context) 1.什么是執(zhí)行上下文簡而言之,執(zhí)行上下文就是當前 JavaScript 代碼被解析和執(zhí)行時所在環(huán)境的抽象概念, JavaScript 中運行任何的代碼都是在執(zhí)行上下文中運行
2.執(zhí)行上下文的類型執(zhí)行上下文總共有三種類型:
全局執(zhí)行上下文: 這是默認的、最基礎(chǔ)的執(zhí)行上下文。不在任何函數(shù)中的代碼都位于全局執(zhí)行上下文中。它做了兩件事:1. 創(chuàng)建一個全局對象,在瀏覽器中這個全局對象就是 window 對象。2. 將 this 指針指向這個全局對象。一個程序中只能存在一個全局執(zhí)行上下文。
函數(shù)執(zhí)行上下文: 每次調(diào)用函數(shù)時,都會為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文。每個函數(shù)都擁有自己的執(zhí)行上下文,但是只有在函數(shù)被調(diào)用的時候才會被創(chuàng)建。一個程序中可以存在任意數(shù)量的函數(shù)執(zhí)行上下文。每當一個新的執(zhí)行上下文被創(chuàng)建,它都會按照特定的順序執(zhí)行一系列步驟,具體過程將在本文后面討論。
Eval 函數(shù)執(zhí)行上下文: 運行在 eval 函數(shù)中的代碼也獲得了自己的執(zhí)行上下文,但由于 Javascript 開發(fā)人員不常用 eval 函數(shù),所以在這里不再討論。
二、執(zhí)行上下文的生命周期執(zhí)行上下文的生命周期包括三個階段:創(chuàng)建階段→執(zhí)行階段→回收階段,本文重點介紹創(chuàng)建階段。
1.創(chuàng)建階段當函數(shù)被調(diào)用,但未執(zhí)行任何其內(nèi)部代碼之前,會做以下三件事:
創(chuàng)建變量對象:首先初始化函數(shù)的參數(shù)arguments,提升函數(shù)聲明和變量聲明。下文會詳細說明。
創(chuàng)建作用域鏈(Scope Chain):在執(zhí)行期上下文的創(chuàng)建階段,作用域鏈是在變量對象之后創(chuàng)建的。作用域鏈本身包含變量對象。作用域鏈用于解析變量。當被要求解析變量時,JavaScript 始終從代碼嵌套的最內(nèi)層開始,如果最內(nèi)層沒有找到變量,就會跳轉(zhuǎn)到上一層父作用域中查找,直到找到該變量。
確定this指向:包括多種情況,下文會詳細說明
在一段 JS 腳本執(zhí)行之前,要先解析代碼(所以說 JS 是解釋執(zhí)行的腳本語言),解析的時候會先創(chuàng)建一個全局執(zhí)行上下文環(huán)境,先把代碼中即將執(zhí)行的變量、函數(shù)聲明都拿出來。變量先暫時賦值為undefined,函數(shù)則先聲明好可使用。這一步做完了,然后再開始正式執(zhí)行程序。
另外,一個函數(shù)在執(zhí)行之前,也會創(chuàng)建一個函數(shù)執(zhí)行上下文環(huán)境,跟全局上下文差不多,不過 函數(shù)執(zhí)行上下文中會多出this arguments和函數(shù)的參數(shù)。
2.執(zhí)行階段執(zhí)行變量賦值、代碼執(zhí)行
3.回收階段執(zhí)行上下文出棧等待虛擬機回收執(zhí)行上下文
三、變量提升和this指向的細節(jié) 1.變量聲明提升大部分編程語言都是先聲明變量再使用,但在JS中,事情有些不一樣:
console.log(a)// undefined var a = 10
上述代碼正常輸出undefined而不是報錯Uncaught ReferenceError: a is not defined,這是因為聲明提升(hoisting),相當于如下代碼:
var a; //聲明 默認值是undefined “準備工作” console.log(a); a=10; //賦值2.函數(shù)聲明提升
我們都知道,創(chuàng)建一個函數(shù)的方法有兩種,一種是通過函數(shù)聲明function foo(){}
另一種是通過函數(shù)表達式var foo = function(){} ,那這兩種在函數(shù)提升有什么區(qū)別呢?
console.log(f1) // function f1(){} function f1() {} // 函數(shù)聲明 console.log(f2) // undefined var f2 = function() {} // 函數(shù)表達式
接下來我們通過一個例子來說明這個問題:
function test() { foo(); // Uncaught TypeError "foo is not a function" bar(); // "this will run!" var foo = function () { // function expression assigned to local variable "foo" alert("this won"t run!"); } function bar() { // function declaration, given the name "bar" alert("this will run!"); } } test();
在上面的例子中,foo()調(diào)用的時候報錯了,而bar能夠正常調(diào)用。
我們前面說過變量和函數(shù)都會上升,遇到函數(shù)表達式 var foo = function(){}時,首先會將var foo上升到函數(shù)體頂部,然而此時的foo的值為undefined,所以執(zhí)行foo()報錯。
而對于函數(shù)bar(), 則是提升了整個函數(shù),所以bar()才能夠順利執(zhí)行。
有個細節(jié)必須注意:當遇到函數(shù)和變量同名且都會被提升的情況,函數(shù)聲明優(yōu)先級比較高,因此變量聲明會被函數(shù)聲明所覆蓋,但是可以重新賦值。
alert(a);//輸出:function a(){ alert("我是函數(shù)") } function a(){ alert("我是函數(shù)") }// var a = "我是變量"; alert(a); //輸出:"我是變量"
function聲明的優(yōu)先級比var聲明高,也就意味著當兩個同名變量同時被function和var聲明時,function聲明會覆蓋var聲明
這代碼等效于:
function a(){alert("我是函數(shù)")} var a; //hoisting alert(a); //輸出:function a(){ alert("我是函數(shù)") } a = "我是變量";//賦值 alert(a); //輸出:"我是變量"
最后我們看個復(fù)雜點的例子:
function test(arg){ // 1. 形參 arg 是 "hi" // 2. 因為函數(shù)聲明比變量聲明優(yōu)先級高,所以此時 arg 是 function console.log(arg); var arg = "hello"; // 3.var arg 變量聲明被忽略, arg = "hello"被執(zhí)行 function arg(){ console.log("hello world") } console.log(arg); } test("hi"); /* 輸出: function arg(){ console.log("hello world") } hello */
這是因為當函數(shù)執(zhí)行的時候,首先會形成一個新的私有的作用域,然后依次按照如下的步驟執(zhí)行:
如果有形參,先給形參賦值
進行私有作用域中的預(yù)解釋,函數(shù)聲明優(yōu)先級比變量聲明高,最后后者會被前者所覆蓋,但是可以重新賦值
私有作用域中的代碼從上到下執(zhí)行
3.確定this的指向先搞明白一個很重要的概念 —— this的值是在執(zhí)行的時候才能確認,定義的時候不能確認! 為什么呢 —— 因為this是執(zhí)行上下文環(huán)境的一部分,而執(zhí)行上下文需要在代碼執(zhí)行之前確定,而不是定義的時候。看如下例子:
// 情況1 function foo() { console.log(this.a) //1 } var a = 1 foo() // 情況2 function fn(){ console.log(this); } var obj={fn:fn}; obj.fn(); //this->obj // 情況3 function CreateJsPerson(name,age){ //this是當前類的一個實例p1 this.name=name; //=>p1.name=name this.age=age; //=>p1.age=age } var p1=new CreateJsPerson("尹華芝",48); // 情況4 function add(c, d){ return this.a + this.b + c + d; } var o = {a:1, b:3}; add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16 add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34 // 情況5
接下來我們逐一解釋上面幾種情況
對于直接調(diào)用 foo 來說,不管 foo 函數(shù)被放在了什么地方,this 一定是 window
對于 obj.foo() 來說,我們只需要記住,誰調(diào)用了函數(shù),誰就是 this,所以在這個場景下 foo 函數(shù)中的 this 就是 obj 對象
在構(gòu)造函數(shù)模式中,類中(函數(shù)體中)出現(xiàn)的this.xxx=xxx中的this是當前類的一個實例
call、apply和bind:this 是第一個參數(shù)
箭頭函數(shù)this指向:箭頭函數(shù)沒有自己的this,看其外層的是否有函數(shù),如果有,外層函數(shù)的this就是內(nèi)部箭頭函數(shù)的this,如果沒有,則this是window。
四、執(zhí)行上下文棧(Execution Context Stack)函數(shù)多了,就有多個函數(shù)執(zhí)行上下文,每次調(diào)用函數(shù)創(chuàng)建一個新的執(zhí)行上下文,那如何管理創(chuàng)建的那么多執(zhí)行上下文呢?
JavaScript 引擎創(chuàng)建了執(zhí)行上下文棧來管理執(zhí)行上下文。可以把執(zhí)行上下文棧認為是一個存儲函數(shù)調(diào)用的棧結(jié)構(gòu),遵循先進后出的原則。
從上面的流程圖,我們需要記住幾個關(guān)鍵點:
JavaScript執(zhí)行在單線程上,所有的代碼都是排隊執(zhí)行。
一開始瀏覽器執(zhí)行全局的代碼時,首先創(chuàng)建全局的執(zhí)行上下文,壓入執(zhí)行棧的頂部。
每當進入一個函數(shù)的執(zhí)行就會創(chuàng)建函數(shù)的執(zhí)行上下文,并且把它壓入執(zhí)行棧的頂部。當前函數(shù)執(zhí)行完成后,當前函數(shù)的執(zhí)行上下文出棧,并等待垃圾回收。
瀏覽器的JS執(zhí)行引擎總是訪問棧頂?shù)膱?zhí)行上下文。
全局上下文只有唯一的一個,它在瀏覽器關(guān)閉時出棧。
我們再來看個例子:
var color = "blue"; function changeColor() { var anotherColor = "red"; function swapColors() { var tempColor = anotherColor; anotherColor = color; color = tempColor; } swapColors(); } changeColor();
上述代碼運行按照如下步驟:
當上述代碼在瀏覽器中加載時,JavaScript 引擎會創(chuàng)建一個全局執(zhí)行上下文并且將它推入當前的執(zhí)行棧
調(diào)用 changeColor函數(shù)時,此時changeColor函數(shù)內(nèi)部代碼還未執(zhí)行,js執(zhí)行引擎立即創(chuàng)建一個changeColor的執(zhí)行上下文(簡稱EC),然后把這執(zhí)行上下文壓入到執(zhí)行棧(簡稱ECStack)中。
執(zhí)行changeColor函數(shù)過程中,調(diào)用swapColors函數(shù),同樣地,swapColors函數(shù)執(zhí)行之前也創(chuàng)建了一個swapColors的執(zhí)行上下文,并壓入到執(zhí)行棧中。
swapColors函數(shù)執(zhí)行完成,swapColors函數(shù)的執(zhí)行上下文出棧,并且被銷毀。
changeColor函數(shù)執(zhí)行完成,changeColor函數(shù)的執(zhí)行上下文出棧,并且被銷毀。
給大家推薦一個好用的BUG監(jiān)控工具Fundebug,歡迎免費試用!
歡迎關(guān)注公眾號:前端工匠,你的成長我們一起見證!如果你感覺有收獲,歡迎給我打賞,以激勵我更多輸出優(yōu)質(zhì)開源內(nèi)容
參考文章了解JavaScript的執(zhí)行上下文
前端面試之道
深入理解javascript原型和閉包系列
【譯】理解 Javascript 執(zhí)行上下文和執(zhí)行棧
深入理解JavaScript中的作用域和上下文
前端基礎(chǔ)進階(二):執(zhí)行上下文詳細圖解
深入理解JS中聲明提升、作用域(鏈)和this關(guān)鍵字
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/53890.html
摘要:執(zhí)行上下文和執(zhí)行棧是中關(guān)鍵概念之一,是難點之一。理解執(zhí)行上下文和執(zhí)行棧同樣有助于理解其他的概念如提升機制作用域和閉包等。函數(shù)執(zhí)行完成,函數(shù)的執(zhí)行上下文出棧,并且被銷毀。 前言 如果你是一名 JavaScript 開發(fā)者,或者想要成為一名 JavaScript 開發(fā)者,那么你必須知道 JavaScript 程序內(nèi)部的執(zhí)行機制。執(zhí)行上下文和執(zhí)行棧是JavaScript中關(guān)鍵概念之一,是Ja...
摘要:本計劃一共期,每期重點攻克一個面試重難點,如果你還不了解本進階計劃,點擊查看前端進階的破冰之旅本期推薦文章深入之執(zhí)行上下文棧和深入之變量對象,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。 (關(guān)注福利,關(guān)注本公眾號回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進階的第一期,本周的主題是調(diào)用堆棧,今天是第二天。 本計劃一共28期,每期...
摘要:當遇到函數(shù)調(diào)用時,引擎為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文并把它壓入當前執(zhí)行棧的頂部。參考鏈接理解中的執(zhí)行上下文和執(zhí)行棧深入之執(zhí)行上下文棧 開篇 作為一個JavaScript的程序開發(fā)者,如果被問到JavaScript代碼的執(zhí)行順序,你腦海中是不是有一個直觀的印象 -- JavaScript 是順序執(zhí)行的,可事實真的是這樣的嗎? 讓我們首先看兩個小例子: var foo = functio...
摘要:首次運行代碼時,會創(chuàng)建一個全局執(zhí)行上下文并到當前的執(zhí)行棧中。執(zhí)行上下文的創(chuàng)建執(zhí)行上下文分兩個階段創(chuàng)建創(chuàng)建階段執(zhí)行階段創(chuàng)建階段確定的值,也被稱為。 (關(guān)注福利,關(guān)注本公眾號回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進階的第一期,本周的主題是調(diào)用堆棧,,今天是第一天 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進...
摘要:使用上一篇文章的例子來說明下自由變量進階期深入淺出圖解作用域鏈和閉包訪問外部的今天是今天是其中既不是參數(shù),也不是局部變量,所以是自由變量。 (關(guān)注福利,關(guān)注本公眾號回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進階的第二期,本周的主題是作用域閉包,今天是第7天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進階計...
閱讀 1161·2021-11-16 11:45
閱讀 1016·2021-09-04 16:41
閱讀 3077·2019-08-29 16:40
閱讀 2852·2019-08-29 15:34
閱讀 2673·2019-08-29 13:11
閱讀 1734·2019-08-29 12:58
閱讀 1726·2019-08-28 18:00
閱讀 1776·2019-08-26 18:26