摘要:最先執行完畢的一定是最里面的函數,執行過后彈出調用棧,接著執行上一層函數,直至所有函數執行完,調用棧清空。到這里你應該就會明白,上面函數調用棧,就是生成了一個函數的執行上下文。
前言 為什么會有這篇文章?
在書籍或博客上,我們經常會看到「作用域鏈」、「閉包」、「變量提升」等概念,說明一個問題 —— 它們很重要。
但很多時候,對于這些概念,看的時候覺得自己已經明白了,可過不了多久,再讓你說一說,可能就說不清楚了,之所以會這樣,是因為我們對于 JavaScript 這門語言的運行機制不清楚。
我相信搞明白了今天所講的內容,會對你理解那些知識大有裨益!
函數調用棧(call stack) 1. 什么是棧?類似 js 中的數組,棧也是用來存儲數據的一種數據結構。他的特點是后進先出(LIFO)。
與之相對的一種數據結構稱為隊列,隊列的特點是先進先出(FIFO)。
可以想象這樣一種場景:小明和同學們放學回家,老師讓他在排在隊伍的最前面,他們每天回家路上都要經過一個胡同,小明每次都是第一個進入胡同,肯定也是第一個出來,這就是所謂「先進先出」。
可是有一天,小明他們走到胡同里發現胡同口停了一輛車,把胡同給堵死了,沒辦法,他們只能隊頭變隊尾往回撤,這時候,小明雖然最先進入胡同,卻只能最后出去,最先出去的是排在隊尾的小華,也就是「后進先出」。
2. 什么叫函數調用棧?在 js 中函數的調用也遵照這樣以一個原則:最先調用的函數先放到調用棧中,假如這個函數內部又調用了別的函數,那么這個內部函數就接著被放入調用棧中,直至不再有函數調用。最先執行完畢的一定是最里面的函數,執行過后彈出調用棧,接著執行上一層函數,直至所有函數執行完,調用棧清空。
這樣說可能會不太明白,舉個例子:
// 其他語句 function first() { console.log("first") function second() { console.log("second") } second(); third(); // 其他語句 } //其他語句 function third() { console.log("third") } // 調用 first first();
在上述代碼中,首先調用的是函數 first, 此時 first 進入函數棧,接著在 first 中調用函數 second,second 入棧,當 second 執行完畢后,second 出棧,third 入棧,接著 third 執行完出棧,執行 first 其他代碼,直至 first 執行完,函數棧清空。
執行上下文(Execution Context) 1. 什么是執行上下文?js 代碼在執行時,會進入一個執行環境,它會形成一個作用域。這個執行環境,便是執行上下文。
JavaScript 主要有三種執行環境:
全局執行環境: 代碼開始執行時首先進入的環境。
函數環境:函數調用時,會開始執行函數中的代碼。
eval:不建議使用,可忽略。
2. 執行上下文的生命周期上面講到 js 代碼執行時會生成一個執行上下文。而這個執行上下文的周期,分為兩個階段:
創建階段。這個階段會生成變量對象(VO),建立作用域鏈以及確定 this 的值。
執行階段。這個階段進行變量賦值,函數引用及執行代碼。
到這里你應該就會明白,上面函數調用棧,就是生成了一個函數的執行上下文。
執行上下文也同樣遵循函數調用棧的規則,無非就是多加了一層 —— 全局執行上下文,函數執行完后會跳出執行棧,而全局執行上下文,會在關閉瀏覽器后跳出執行棧。
還是上面的例子,我們看一下執行棧。
變量對象 1. 什么叫變量對象?從上面其實可以得到答案,變量對象是 js 代碼在進入執行上下文時,js 引擎在內存中建立的一個對象,用來存放當前執行環境中的變量。
2. 變量對象(VO)的創建過程變量對象的創建,是在執行上下文創建階段,依次經過以下三個過程:
創建 arguments 對象。對于函數執行環境,首先查詢是否有傳入的實參,如果有,則會將參數名是實參值組成的鍵值對放入arguments 對象中,否則,將參數名和 undefined,組成的鍵值對放入 arguments 對象中。
function bar(a, b, c) { console.log(arguments); // [2, 4] console.log(arguments[2]); // undefined } bar(2,4)
檢查當前環境中的函數聲明。當遇到同名的函數時,后面的會覆蓋前面的。
console.log(a); // function a() {console.log("fjdsfs") } function a() { console.log("24"); } function a() { console.log("fjdsfs") }
在上面的例子中,在執行第一行代碼之前,函數聲明已經創建完成,后面的對之前的聲明進行了覆蓋。
檢查當前環境中的變量聲明并賦值為undefined。當遇到同名的函數聲明,為了避免函數被賦值為 undefined ,會忽略此聲明
console.log(a); // function a() {console.log("fjdsfs") } console.log(b); // undefined function a() { console.log("24"); } function a() { console.log("fjdsfs"); } var b = "bbbbbbbb"; var a = 46;
在上例我們可以看到,在代碼之前前,a 仍舊是一個函數,而 b 是 undefined。
根據以上三個步驟,對于變量提升也就知道是怎么回事了。
3. 變量對象變為活動對象執行上下文的第二個階段,稱為執行階段,在此時,會進行變量賦值,函數引用并執行其他代碼,此時,變量對象變為活動對象。
我們還是舉上面的例子:
console.log(a); // function a() {console.log("fjdsfs") } console.log(b); // undefined function a() { console.log("24"); } function a() { console.log("fjdsfs"); } var b = "bbbb"; console.log(b); // "bbbb" var a = 46; console.log(a); // 46 var b = "hahahah"; console.log(b); // "hahah"
在上面的代碼中,代碼真正開始執行是從第一行 console.log() 開始的,自這之前,執行上下文是這樣的:
// 創建過程 EC= { VO: {}; // 創建變量對象 scopeChain: {}; // 作用域鏈 } VO = { argument: {...}; // 當前為全局上下文,所以這個屬性值是空的 a: // 函數 a 的引用地址 b: undefiend // 見上文創建變量對象的第三步 }
根據步驟,首先是 arguments 對象的創建;其次,是檢查函數的聲明,此時,函數 a 聲明了兩次,后一次將覆蓋前一次;最后,是檢查變量的聲明,先聲明了變量 b,將它賦值為 undefined,接著遇到 a 的聲明,由于 a 已經聲明為了一個函數,所以,此條聲明將會被忽略。
到此,變量對象的創建階段完成,接下來時執行階段,我們一步一步來。
執行 console.log(a),我們知道,此時 a 是第二個函數,所以會輸出function a() {...};
執行 console.log(b),不出我們所料,將會輸出 undefined;
執行賦值操作: b = "bbbb";
執行 console.log(b) ,此時,b 已經賦值,所以會輸出 "bbbb";
執行賦值操作: a = 46;
執行 console.log(a) ,此時,a 的值變為 46。
執行賦值操作: b = "hahahah";
執行 console.log(b), b 已經被重新賦值,輸出 hahahah。
由上面我們可以看到,在執行階段,變量對象是跟著代碼不斷變化的,此時,我們把變量對象成為活動對象。
執行到最后一步時,執行上下文變成了這樣。
// 執行階段 EC = { VO = {}; scopeChain: {}; } // VO ---- AO AO = { argument: {...}; a: 46; b: "hahahah"; this: window; }
以上,就是變量對象在代碼執行前及執行時的變化。
剛開始就說過,這部分概念將會對你理解后面的知識有很大的幫助,所以剛開始接觸的話可能會有些晦澀,建議就是認真讀兩遍,結合后面的知識,經常回過頭來看看。
最后留一道題,給大家作為練手,觀察觀察執行上下文及變量對象的變化。
console.log(a); console.log(b); var a = 4; function a() { console.log("我是a1"); b(3, 5); } var a = function a() { console.log("我是a2"); b(3, 5); } var b = function (m, n) { console.log(arguments); console.log("b") } a();
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/99351.html
摘要:最先執行完畢的一定是最里面的函數,執行過后彈出調用棧,接著執行上一層函數,直至所有函數執行完,調用棧清空。到這里你應該就會明白,上面函數調用棧,就是生成了一個函數的執行上下文。 showImg(http://upload-images.jianshu.io/upload_images/7803415-36e8e7d048f63524.jpg?imageMogr2/auto-orient...
摘要:最先執行完畢的一定是最里面的函數,執行過后彈出調用棧,接著執行上一層函數,直至所有函數執行完,調用棧清空。到這里你應該就會明白,上面函數調用棧,就是生成了一個函數的執行上下文。 showImg(http://upload-images.jianshu.io/upload_images/7803415-36e8e7d048f63524.jpg?imageMogr2/auto-orient...
摘要:以上簡單總結了下對執行上下文和變量對象的理解,主要在于記錄總結一下學習成果,目前文章的水平實在不敢談分享。 執行上下文(Execution Context) 文章同步到github javaScript中的執行上下文和變量對象 JavaScript代碼執行的過程,包括編譯和執行兩個階段,編譯就是通過詞法分析,構建抽象抽象語法樹,并編譯成機器識別的指令,在JavaScript代碼編譯階段...
摘要:在中,通過棧的存取方式來管理執行上下文,我們可稱其為執行棧,或函數調用棧。而處于棧頂的是當前正在執行函數的執行上下文,當函數調用完成后,它就會從棧頂被推出理想的情況下,閉包會阻止該操作,閉包后續文章深入詳解。 寫在開篇 已經不敢自稱前端小白,曾經吹過的牛逼總要一點點去實現。 正如前領導說的,自己喝酒吹過的牛皮,跪著都得含著淚去實現。 那么沒有年終完美總結,來個新年莽撞開始可好。 進擊巨...
摘要:在中,通過棧的存取方式來管理執行上下文,我們可稱其為執行棧,或函數調用棧。因為執行中最先進入全局環境,所以處于棧底的永遠是全局環境的執行上下文。 一、什么是執行上下文? 執行上下文(Execution Context): 函數執行前進行的準備工作(也稱執行上下文環境) JavaScript在執行一個代碼段之前,即解析(預處理)階段,會先進行一些準備工作,例如掃描JS中var定義的變量、...
閱讀 3258·2021-10-11 10:59
閱讀 2812·2021-10-11 10:58
閱讀 2244·2021-09-04 16:45
閱讀 2717·2019-08-30 15:44
閱讀 671·2019-08-30 15:44
閱讀 3199·2019-08-30 10:51
閱讀 1597·2019-08-29 18:46
閱讀 2749·2019-08-29 13:57