在書籍或博客上,咱們常常會看到「做用域鏈」、「閉包」、「變量提高」等概念,說明一個問題 —— 它們很重要。git
但不少時候,對於這些概念,看的時候以爲本身已經明白了,可過不了多久,再讓你說一說,可能就說不清楚了,之因此會這樣,是由於咱們對於 JavaScript 這門語言的運行機制不清楚。github
我相信搞明白了今天所講的內容,會對你理解那些知識大有裨益!數組
相似 js 中的數組,棧也是用來存儲數據的一種數據結構。他的特色是後進先出(LIFO)。瀏覽器
與之相對的一種數據結構稱爲隊列,隊列的特色是先進先出(FIFO)。數據結構
能夠想象這樣一種場景:小明和同窗們放學回家,老師讓他在排在隊伍的最前面,他們天天回家路上都要通過一個衚衕,小明每次都是第一個進入衚衕,確定也是第一個出來,這就是所謂「先進先出」。閉包
但是有一天,小明他們走到衚衕裏發現衚衕口停了一輛車,把衚衕給堵死了,沒辦法,他們只能隊頭變隊尾往回撤,這時候,小明雖然最早進入衚衕,卻只能最後出去,最早出去的是排在隊尾的小華,也就是「後進先出」。函數
在 js 中函數的調用也遵守這樣以一個原則:最早調用的函數先放到調用棧中,假如這個函數內部又調用了別的函數,那麼這個內部函數就接着被放入調用棧中,直至再也不有函數調用。最早執行完畢的必定是最裏面的函數,執行事後彈出調用棧,接着執行上一層函數,直至全部函數執行完,調用棧清空。this
這樣說可能會不太明白,舉個例子:spa
// 其餘語句 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
執行完,函數棧清空。3d
js 代碼在執行時,會進入一個執行環境,它會造成一個做用域。這個執行環境,即是執行上下文。
JavaScript
主要有三種執行環境:
eval
:不建議使用,可忽略。上面講到 js 代碼執行時會生成一個執行上下文。而這個執行上下文的週期,分爲兩個階段:
到這裏你應該就會明白,上面函數調用棧,就是生成了一個函數的執行上下文。
執行上下文也一樣遵循函數調用棧的規則,無非就是多加了一層 —— 全局執行上下文,函數執行完後會跳出執行棧,而全局執行上下文,會在關閉瀏覽器後跳出執行棧。
仍是上面的例子,咱們看一下執行棧。
從上面其實能夠獲得答案,變量對象是 js 代碼在進入執行上下文時,js 引擎在內存中創建的一個對象,用來存放當前執行環境中的變量。
變量對象的建立,是在執行上下文建立階段,依次通過如下三個過程:
建立 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。
根據以上三個步驟,對於變量提高也就知道是怎麼回事了。
執行上下文的第二個階段,稱爲執行階段,在此時,會進行變量賦值,函數引用並執行其餘代碼,此時,變量對象變爲活動對象。
咱們仍是舉上面的例子:
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 reference> // 函數 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();
原文地址: 阿木木的博客