當代碼執行時都會產生一個執行環境。JavaScript中的執行環境能夠分爲三種。前端
函數內,沒有使用var 聲明的變量,在非嚴格模式下爲window的屬性,即全局變量。
java
js 是根據函數的調用(執行) 來決定 執行順序的。每當一個函數被調用時,js 會爲其建立執行環境,js引擎就會把這個執行環境 放入一個棧中 來處理。瀏覽器
這個棧,咱們稱之爲函數調用棧(call stack)。棧底永遠都是全局環境,而棧頂就是當前正在執行函數的環境。當棧頂的執行環境 執行完以後,就會出棧,並把執行權交給以前的執行環境。閉包
看栗子說話:函數
function A(){ console.log("this is A"); function B(){ console.log("this is B"); } B(); } A();
那麼這段代碼執行的狀況就是這樣了。this
再來個不常規的:spa
function A(){ function B(){ console.log(say); } return B; } var C = A(); C();
眼尖的同窗,估計看出來了,它怎麼像閉包呢?其實,稍微改動下,它就是閉包了。設計
function A(){ var say = 666 function B(){ console.log(say); } return B; } var C = A(); C(); //666
這就是閉包了,可是此次咱們不講閉包,你就知道,它是的執行是怎麼回事就行。
3d
如今咱們已經知道,每當一個函數執行時,一個新的執行環境就會被建立出來。其實,在js引擎內部,這個環境的建立過程可分爲兩個階段:code
A. 創建階段(發生在調用(執行)一個函數時,可是在執行函數內部的具體代碼以前)
1.創建活動對象; 2.構建做用域鏈; 3.肯定this的值。
B. 代碼執行階段(執行函數內部的具體代碼)
1.變量賦值;
2.執行其它代碼。
須要注意的是,做用域鏈是建立函數的時候就建立了,此時的鏈只有全局變量對象,保存在函數的[[Scope]]屬性中,而後函數執行時的,只是經過複製該屬性中的對象 來 構建做用域鏈。本文後面還有說明。
看圖更清晰!
若是把函數執行環境當作一個對象的話:
executionContextObj = { //執行上下文對象 AtiveObject: { }, //活動對象 scopeChain: { }, //做用域鏈 this: {} //this }
//下面這段內容,感興趣的能夠看下,不感興趣,就跳過哈。
也許你在別家看到跟個人不同,人家寫的是創建變量對象。下面我來講說我得想法吧!
以前我按照 首先創建變量對象,其後,變量對象轉變爲活動對象的規則 去理解,可是呢,經過我分析JavaScript高級程序設計第三版,4.2節 和 7.2節,發現根本就不符合邏輯。
而後,我根據分析,得出了個人結論:變量對象 是執行環境中保存着環境中定義的全部變量和函數 的對象 的統稱。而活動對象,是函數執行環境中建立的,它不只保存着函數執行環境中定義的變量和函數,而且獨有一個arguments 屬性。所以,活動對象也可稱之爲變量對象。
這樣,不少東西就說的通了。
好比(如下都是來自JavaScript高級程序設計第三版,4.2節 和 7.2節 中原文):
若是這個環境是函數,則將其活動對象(activation object)做爲變量對象。活動對象在最開始時只包含一個變量,即 arguments 對象(這個對象在全局環境中是不存在的)。
當某個函數被調用時,會建立一個執行環境(execution context)及相應的做用域鏈。
而後,使用 arguments 和其餘命名參數的值來初始化函數的活動對象。
每一個執行環境都有一個表示變量的對象——變量對象。
此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執
行環境做用域鏈的前端。對於這個例子中 compare() 函數的執行環境而言,其做用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。
有興趣的能夠去看看這本書上說的,有不一樣的想法能夠積極留言,我們好好探討,哈哈。
一、創建活動對象(AO)
A. 創建arguments對象,檢查當前上下文中的參數,創建該對象下的屬性以及屬性值 。
B. 檢查當前環境中的函數聲明(使用function 聲明的)。每找到一個函數聲明,就在活動對象下面用函數名創建一個屬性,屬性值就是指向該函數在內存中的地址的一個引用,若是上述函數名已經存在於活動對象下,那麼則會被新的函數引用所覆蓋。
C. 檢查當前上下文中的變量聲明(使用 var 聲明的)。每找到一個變量聲明,就在活動對象下面用變量名創建一個屬性,該屬性值爲undefined。若是該屬性名已存在,則忽略新的聲明。
function test(){ function a(){}; var b; } test();
test 函數 的活動對象:
testAO: { //test變量對象 arguments: { ... }; a:function(){}; b:undefined }
變量做用域
javaScript 中,只有兩種變量做用域,一種是局部變量做用域,又稱函數做用域。另外一個則是全局做用域。
什麼變量提高問題的根本緣由就在創建階段了。
console.log(A); function A(){}; console.log(B); var A = 666; var B = 566; console.log(A); console.log(B); //function A //undefined //666 //566
上面的實際順序就是這樣的了
function A(){}; //var A; 這個var 聲明的 同名 A,會被忽略 var B = undefined; console.log(A); console.log(B); A = 666; //給A 從新賦值 B = 566; //給B 賦值 console.log(A); console.log(B);
注意第三點,使用var 聲明時,若是VO對象下,該屬性已存在,忽略新的var 聲明。
由於A 使用 function 聲明,VO對象下,建立A屬性,而後 var 聲明時,檢索發現已經有該屬性了,就會忽略 var A 的聲明,不會把A 設置爲 undefined。
二、構建做用域鏈
做用域鏈的最前端,始終都是當前執行的代碼所在函數的活動對象。下一個AO(活動對象)爲包含本函數的外部函數的AO,以此類推。最末端,爲全局環境的變量對象。
注意:雖然做用域鏈是在函數調用時構建的,可是,它跟調用順序(進入調用棧的順序)無關,由於它只跟 包含關係(函數 包含 函數 的嵌套關係) 有關。
可能比較繞口,仍是來個小栗子,再來個圖
function fa(){ var va = "this is fa"; function fb(){ var vb = "this is fb"; console.log(vb); console.log(va); } return fb; } var fc = fa(); fc(); //"this is fb" //"this is fa"
函數調用棧的狀況就是這樣:
那麼把函數 fb 的執行環境比做對象(創建階段):
fbEC = { //執行上下文對象 fbAO: { //活動對象 AO arguments: { ... }; //arguments 對象 vb: undefined //變量聲明創建的屬性,設置爲undefined }, scopeChain: [ AO(fa), AO(fb), VO(window) ], //做用域鏈 this: { ... } //this }
fb做用域的展開就是這樣的:
fb 函數 被 fa 函數 包含, fa 函數 被 window 全局環境包含。做用域鏈只跟包含關係有關!
注意:做用域鏈是單向的,所以,函數內的能夠訪問函數外 和 全局的變量,函數,可是反過來,函數外,全局內 不能訪問函數內的變量,函數。
三、肯定 this 指向
因此說 this 的指向,是在函數執行時肯定的。
一、變量賦值
根據代碼順序執行,遇到變量賦值時, 給對應的變量賦值。
function getColor(){ console.log(color); var color; console.log(color); color = "red"; console.log(color); } getColor(); //undefined //undefined //"red";
三、執行其餘代碼。
當函數執行完畢後,局部活動對象就會被銷燬(也就是說,局部的變量,函數,arguments 等都會被銷燬),內存中僅保存全局做用域(全局執行環境的變量對象)。
這句話對理解閉包很重要,隨後,我會出一個閉包的文章,敬請期待!