在上一篇文章“執行環境和作用域”中,我試著梳理了執行環境和作用域的關系。但實際上,文章中并沒有提到作用域,而是介紹了執行環境和作用域鏈。這里先把上篇文章的坑填了。
上篇文章的最后,我提了一個問題。這里把代碼稍微修改下,如下:
var name = 'window';
outer();
function outer(){
var name = 'outer';
inner(); //輸出什么?
}
function inner(){
console.log(name);
}
這段代碼很簡單,但很容易迷惑人。很多人可能會認為,應該輸出“outer”,但實際上結果確實“window”。這是為什么呢?
1、作用域和執行上下文的區別
作用域和執行上下是兩個不同的概念。執行上下文在上篇文章中已經解釋過了:在函數執行時創建。那么,作用域怎么理解呢?
作用域可以理解為一套規則,這套規則用來管理引擎如何在當前作用域以及嵌套的子作用域中根據標識符名稱進行變量查找。同執行環境一樣,作用域只有兩種(不考慮eval):全局作用域與函數作用域。
在js中代碼整個的執行分為兩個階段:代碼編譯和代碼執行。代碼編譯由編譯器完成,將代碼翻譯成可執行代碼。代碼執行由js引擎完成,主要任務是執行可執行的代碼。在代碼編譯階段,作用域規則就已經被確定了。到代碼執行時,執行上下文被創建,同時,作用域鏈作為作用域規則的具體實現被構建出來。過程如下圖:
再回頭去看開頭的問題,就不難理解了:在編譯階段,inner函數的相關的作用域規則就已經確定了,在而outer函數中執行時,只是具體地實現了相關的作用域的規則,也就是構建作用域鏈,而這個作用域鏈上面沒有outer函數執行環境相對應的變量對象,而是有全局執行環境對應的window對象,因此,結果是‘window’。
2、閉包
閉包在js高程中的解釋是:有權訪問另一個函數作用域中的變量的函數。簡單說就是,假設函數a是定義在函數b中的函數,那么函數a就是一個閉包。正常情況下,在函數的外部訪問不到函數內部的變量,但有了閉包就可以間接的實現訪問內部變量的需要。也就是說,閉包是連接函數內部和外部的橋梁。這就是閉包的第一個作用:訪問函數內部的變量。還有另外一個作用就是:讓被引用的變量值始終保持在內存中。
在上一篇文章中提到代碼當執行到一個函數時,會創建一個臨時的活動對象,并把這個對象作為變量對象推入環境棧中。當這個函數執行完的時候,這個對象就會出桟,并被銷毀。但當閉包中引用了函數中的變量時,那么,這個變量就會保存在內存中。也就是上面提到的閉包的第二個作用。之所以為這樣,是因為JavaScript的回收機制。
基本所有瀏覽器都是使用“標記清除”的方式回收內存。也就是說,當變量進入執行環境的時候(在函數中聲明一個變量),就給變量添加標記,而當函數執行完的,變量不再被引用的時候,再添加刪除的標記,垃圾收集器就會自動清楚這個變量占有的內存。但在閉包中引用了函數中的變量,而閉包又被當作結果返回時,閉包中的因為被引用就不會被清除。例如,下面的代碼:
function fn1(){
var a = 1;
return function(){
console.log(++a);
}
}
var fn2 = fn1();
fn2(); //輸出2
fn2(); //輸出3
在這段代碼中,fn1中的閉包函數被當作結果返回,在閉包中的引用的變量a因為被引用而沒有被清除,一直保存在內存當中,所以執行fn2的時候會輸出不斷增加的結果:2和3。
使用閉包時需要注意的問題
1、由于閉包會使得函數中被引用的變量一直保存在內存中,消耗內存,所以謹慎使用閉包,否則會造成網頁的性能問題。
2、閉包會改變父函數內部變量的值。如果父函數再次被執行的,而在外部已經執行過閉包修改變量的值,那么,這次執行的結果就會和上次的不一樣。
最后再來一段代碼,想下輸出的代碼,沒疑問的化,閉包的運行機制基本就掌握了:
var name = "window";
var obj = {
name : "obj",
getName : function(){
return function(){
return this.name;
};
}
};
console.log(obj.getName()());
寫在結尾:
如果覺得我寫的文章對你有幫助,歡迎掃碼關注我的公眾號:海痕筆記
微信號:haihenbiji
