理解javascript中的做用域和做用域鏈對咱們理解js這們語言。此次想深刻的聊下關於js執行的內部機制,
主要討論下,做用域,做用域鏈,閉包的概念。爲了更好的理解這些東西,我模擬了當一個函數執行時,js引擎作了哪些事情--那些咱們看不見的動做。javascript
關鍵詞:java
咱們都知道js的執行環境最外層是一個全局環境Global,在web瀏覽器的宿主環境下,window對象被認爲是全局執行環境。在後臺的nodejs環境global做爲全局變量也是咱們能夠直接訪問到的。
某個執行環境中全部代碼執行完畢後,該環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬(全局環境到應用退出--如關閉網頁或瀏覽器)node
每一個函數也有本身的執行環境,當執行流進入函數時,函數的環境被推入一個環境棧中,函數執行完畢以後,棧將其環境彈出,把控制權返回給以前的執行環境。web
當代碼在一個環境中執行時,會建立建立變量對象的一個做用域鏈
。
若是環境是個函數,則將其活動對象做爲變量對象。活動對象在最開始只包含一個變量,即arguments對象,做用域鏈的下一個變量對象來自下一個包含環境,一直延續到全局環境。數組
下面咱們模擬下這個過程。瀏覽器
var name = "eric"; function say(){ var name = "xu"; console.log(name); } say();//xu
輸出「xu」,而不是「eric」,這個咱們也許都很好理解,由於函數內部定義了局部同名變量name,而不會使用全局的name。上面的環境中包含全局變量name
和say
函數;當say執行時,js引擎作了些什麼。下面咱們模擬下引擎「偷偷」爲咱們作的事。閉包
首先say()執行時會建立一個執行環境,爲了形象一些,我這裏以三個大括號可視化表示一個執行環境。如:say(){{{...}}}函數
這個執行環境中會自動擁有一個特殊的內部屬性[[Scope]]
(爲了更好的理解,能夠把它想象成若是是全局環境的window,全局環境定義的變量和函數附着在這個變量上自動成爲window的屬性和方法,這樣的一個局部功能「局部內全局對象」。但其實局部的變量和函數會被附着在其活動對象上,活動對象又是做用域鏈第一個變量對象。)this
函數調用時與執行環境同時建立的就是相應的做用域鏈
[[Scope Chain]],並賦值給特殊變量Scope;指針
//step 1:建立執行環境,爲了形象一些,我這裏以三個大括號可視化表示一個執行環境 {{{...}}}
//step 2:建立做用域鏈,並賦值給特殊變量Scope,咱們用數組來模擬這個做用域鏈,隨後我會解釋爲何用數組模擬 var ScopeChain = [ FirstVariableObject,//函數內的變量對象 SecondVariableObject //包含這個函數的外面一層的變量對象,在上面的例子中已是全局環境了。 ] Scope = ScopeChain;
在做用域鏈生成以前,其實還有步驟,那就是做用域鏈數組的兩個變量對象的生成。那這兩個變量對象是什麼呢?
其實第一個變量對象就是函數的活動對象
【activation object】,這個活動對象能夠理解成這樣一個對象
ActivationObject = { arguments: [] //活動對象最開始僅包含arguments(就是函數內隱藏的arguments) }
而後內部this根據環境,加入活動對象
ActivationObject = { arguments: [], //活動對象最開始僅包含arguments(就是函數內隱藏的arguments) this: window //這裏的this根據執行環境和調用對象的不一樣,會動態變化,上面的例子由於是全局環境執行的因此this指向window }
而後開始尋找var的變量定義,或者函數聲明(咱們都知道的函數聲明會被提高)。
此時的活動對象變成:
//活動對象,即函數內部全部變量的綜合,會自動成爲第一個變量對象 ActivationObject = { arguments: [], this: window, name: undefined //注意引擎此時並不會初始化賦值,只有讀到賦值那一行時纔會賦值 }
這樣咱們就能很好的理解咱們熟悉的經典例子,爲何下面的console.log不會報錯,也不是輸出'xu',而是undefined
<script> console.log(name);//undefined var name = 'xu'; </script>
由於咱們的活動對象會自動變爲第一個活動對象,因此第一個變量對象就等於活動對象
FirstVariableObject = ActivationObject;
同理做用域中的第二個變量對象SecondVariableObject,或者咱們也能夠命名爲GlobalVariableObject,由於在上面的例子中已是全局環境了
//做用域鏈的第二個,也是最後一個(全局變量對象) SecondVariableObject = { this: window, say: function (){...}, name: "eric" }
第二個變量對象不包含arguments,由於它是全局環境,而不是函數。say函數聲明被提高做爲window的全局方法,還有全局的name屬性。都被掛在第二層的做用域鏈的變量對象上。
至此做用域鏈建立完畢。做用域鏈會成爲這樣的好理解的樣子:
//形象的做用域鏈 Scope = ScopeChain = [ { arguments: [], this: window, name: undefined }, { this: window, say: function (){...}, name: "eric" } ]
而後js開始一句一句解析say函數的代碼,
第一句,var name = "xu"
此時,活動對象的name值纔會將undefined變爲'xu';
而後執行第二句console.log(name);
這句中有一個變量name
,這個時候做用域鏈就該出場了。
js引擎會開始執行查找,首先從ActivationObject活動對象中開始找,由於通過var name = "eric";
此時做用域鏈的第一個,即活動對象已經變成
{ arguments: [], this: window, name: 'xu' }
因此輸出‘xu’,而不是‘eric’
若是咱們將say函數,作下改動以下:
var name = "eric"; function say(){ var age = 99; console.log(name); } say();//eric
由於內部的沒有定義name變量,這個結果不出意料的咱們都知道,但這個過程我把它模擬成如下查找過程:
//從當前函數的活動對象開始,一層一層向上查找,直到頂層全局做用域 //break這句至關重要,當前這一層找到了,再也不向上一層找了。即在這一層環境中找到了變量name for (var i=0;i<Scope.length;i++){ if (name in Scope[i]){ console.log(Scope[i].name); break; } }
我以爲這段代碼,能夠很是形象的表達了做用域鏈的查找過程
,
即首先查找第一個變量對象,其實就是函數內部的活動對象,若是找到則不進行下一個變量對象的查找,若是內部函數沒有,纔會沿着做用域鏈找下一個值,直到頂層的全局環境。
這就是爲何我用數組去模擬做用域鏈的緣由,由於做用域鏈能夠理解是個有序列表(其實做用域鏈的本質就是指向變量對象的指針列表)
,查找過程是按順序查找的。
經過上面的形象化解釋,是否是很是好理解做用域和做用域鏈了呢!!!
咱們都知道在函數執行完畢以後,內部的變量和內部定義的函數會隨之銷燬,也就是被垃圾回收機制所回收,以下:
function talk(){ var name = 'eric'; function say(){ console.log(name); } say(); } talk();
當talk函數執行後,內部的變量name
和聲明的函數say
會從內存中銷燬,但閉包的狀況就不會。如:
function createTalk(){ var name = 'eric'; var age = 99; return function (){ var innerName = name; console.log(innerName); } } var talk = createTalk(); talk();
閉包的本質實際上是有權訪問另外一個函數做用域中變量的函數
根據咱們上面模擬的做用域鏈模型,上面的例子中當talk執行時,整個做用域鏈能夠形象化爲:
ScopeChain = [ { arguments:[], this: window, innerName: undefined }, { arguments:[], this: window, name: eric, age: 99 }, { this: window, createTalk: function (){...}, talk: function (){...} //內部return的匿名函數 }, ]
這樣當createTalk執行後,talk變量仍然保持了對函數內部變量和內部匿名函數的引用,所以即便createTalk執行完畢,雖然其執行環境被銷燬,但返回的匿名函數的做用域鏈被初始化爲createTalk()函數的活動對象和全局變量對象,內部變量仍然沒有被垃圾回收機制所回收。雖然返回的匿名函數,僅使用了外一層的name變量,而沒有使用age變量。但其內部保存的仍然是整個外層變量對象,即
{ arguments:[], this: window, name: eric, age: 99 }
而不只僅是外層的name變量一個值,由於查找過程當中,使用的是整個的變量對象來查找的。由於是查找,因此存在遍歷整個對象的過程,而不是簡單的賦值
。
這就是爲何閉包會佔用更多的內存的緣由,由於其保存了整個變量對象。雖然咱們的例子可能就幾個,但在實際應用中可能存在很是多。
這也是咱們要謹慎使用閉包的緣由。
接下來咱們看一個經典的閉包示例。
var result = []; for (var i=0;i<10;i++){ result[i] = function (){ return i; } }
結果或許你們都知道了,result數組的任何一個執行,都會返回10。下面咱們用上面模擬的做用鏈,形象話的看下,
好比result[9]()函數執行的初始化做用域鏈以下:
ScopeChain = [ //第一層是內部匿名函數的變量對象 { arguments:[], this: window }, //第二層是外部的,也就是全局變量對象 { this: window, result: [Array], i: 10 //此時全局環境的i已經通過for循環變成了10 }, ]
天然任何一個result的值調用函數,都會是返回10。
經過變形符合預期的閉包以下:
var result = []; for (var i=0;i<10;i++){ result[i] = function (num){ return function (){ return num; } }(i); }
上面這個經典的閉包返回的就是咱們想要的各自的i,爲了更好理解,我仍是使用形象的做用域鏈。
當匿名函數執行時,看下它的初始做用域鏈:
ScopeChain = [ //第一層爲傳入參數i的自執行函數 { arguments:[], this: window, }, { arguments:[num], num: 9, this: window, } { this: window, result: [Array], i: 10 } ]
咱們能夠理解爲多了一層做用域鏈的變量對象,使其能保留對num副本的引用,而不是對i的引用。
好了,經過深刻理解做用域鏈,咱們能跟好的理解js的運行機制和閉包的原理。