JS閉包的理解
一、變量的作用域
二、如何從外部讀取局部變量
三、什么是閉包
四、深入理解閉包
五、閉包的用途
六、使用閉包注意情況
七、JavaScript的垃圾回收機制
八、一些思考題
一、變量作用域
1、要理解閉包,我們先來看一下JavaScript的特殊的變量作用域。
變量的作用域無非是兩種,全局變量和局部變量。
JavaScript語言的獨特之處是:函數內部可以讀取所有的全局變量。
var n=999; function f1(){ alert(n); } f1(); // 999
函數外部無法讀取函數內部的局部變量。
function f1(){ var n=999; } alert(n); // error
這里有一個地方需要注意,函數內部聲明變量的時候,一定要使用var命令。如果不用的話,你實際上聲明了一個全局變量!
function f1(){ n=999; } f1(); alert(n); // 999
再來看一個例子,理解一下變量作用域
function a(){ var n = 0; function inc() { n++; console.log(n); } inc(); inc(); } a(); //控制臺輸出1,再輸出2
function a(){ var n = 0; this.inc = function () { n++; console.log(n); }; } var c = new a(); c.inc(); //控制臺輸出1 c.inc(); //控制臺輸出2
function a(){ var n = 0; function inc(){ n++; console.log(n); } return inc; } var c = a(); c(); //控制臺輸出1 c(); //控制臺輸出2
注意:函數名只是一個標識(指向函數的指針),而()才是執行函數
二、如何從外部讀取局部變量
從局部變量讀取外部全局變量是JavaScript語言的特點,但是有時我們需要從外部讀取函數內部的局部變量,那怎么辦呢?解決辦法:在函數內部定義一個函數。
function f1(){ n=999; function f2(){ alert(n); // 999 } }
在函數f1()中定義一個f2()函數。上述代碼中,函數f2倍包括早函數f1內部,這時候,f1的內部局部變量f2都可見,反過來,f2的布局變量,f1不可見。這個就是JavaScript語言特有的“鏈式作用域”的結構---子對象會一級級的向上尋找所有的父對象的變量(即,所有的父對象的變量,子對象都是可見的)。
既然f2可以讀取f1中的局部變量,那么只要把f2作為返回值,我們就可以在f1的外部就可以讀取f1內部變量了。
function f1(){ n=999; function f2(){ alert(n); } return f2; } var result=f1(); result(); // 999
三、什么是閉包
閉包就是可以讀取其他函數內部的變量。
由于,JavaScript語言中,只有函數內部的子函數才能讀取局部變量,也可以說,閉包就是“定義在一個函數內部的函數”。
所以,本質上說,閉包就是講函數內部很函數外部連接起來的一座橋梁。
JavaScript中所有的function都是一個閉包。不過一般來說,嵌套的function所產生的閉包更為強大,也是大部分時候我們所謂的“閉包”。看下面這段代碼:
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c();
這段代碼有個特點:
1、函數b嵌套在函數a內部
2、函數a返回函數b
執行代碼c=a();實際c指向函數b
再執行c();就會彈出一個窗口顯示 i 的值(第一次為1)。
這段代碼其實就是創建了一個閉包,因為函數a外的變量c引用了函數a內的函數b,
當函數a 的 內部函數b 被 函數a 外的 一個變量引用的時候,就創建了一個閉包。
四、深入理解閉包
如果要深入理解閉包和嵌套函數b的關系,我們需要引入:函數的執行環境(excution context)、活動對象c(call object)、作用域(scope)、作用域鏈(scope chain),以函數a為例,從定義到執行過程
1、當定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)設置成 定義a時a所在的環境,如果a是一個全局函數,則作用域鏈(scope chain)中只用window對象。
2、當執行函數a的時候,a會進入到相應的執行環境(excution context)。
3、在創建執行環境的過程中,首先會為a添加一個scope屬性,即a的作用域,其值為第1步的作用域鏈(scope chain)。即a.scope=a的作用域鏈。
4、然后執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型,而且不能通過JavaScript代碼直接訪問。創建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動的對象和window對象。
5、下一步是在活動對象上添加一個arguments屬性,它保存著調用函數a時所傳遞的參數。
6、最后把所有的函數a的形參和內部函數b的引用也添加到a的活動對象上。這一步中,完成了函數b的定義,因此,如同第3步,函數b的作用域鏈被設置成b所定義的環境,即a的作用域
到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的作用域鏈包含鏈包含了對函數a的活動對象的引用,也就是說b可以訪問a中的定義的所有變量和函數。函數b被c引用,函數b依賴函數a,一次函數a在返回后不會被gc回收。
當函數b執行的時候也會像以上步驟一樣。因此,執行時b的作用域鏈包含3個對象:b的活動對象,a的活動對象和window對象,如下圖所示:
如圖所示,在當函數b中訪問一個變量的時候,搜索順序是:
1、先搜索自身的活動對象,如果存在則返回,如果不存在將繼續搜索函數a的活動對象,依次查找,直到找到為止。
2、如果函數b存在prototype原型對象,則在查找完自身的活動對象后先查找自身的原型對象,再繼續查找。這就是JavaScript中的變量查找機制。
3、如果整個作用域鏈都無法找到,則返回undefined。
本節小結,上文提到兩個重要的詞語:函數的定義與執行。文中提到函數的作用域是在定義函數時確定的,而不是在執行的時候確定的(參看步驟1和3),用一段代碼來說明這個:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
這段代碼中,變量h指向了 f 中的那個匿名函數(由g返回)。
1、假設函數h的作用域是在執行alert(h())確定的,那么此時h的作用域鏈是:h的活動對象-》alert活動對象-》window對象。
2、假設函數h的作用域是在定義時候確定的就是說h指向的那個匿名函數在定義的時候就已經確定確定了作用域。那么在執行的時候,h的作用域鏈為:h的活動對象-》f的活動對象-》window活動對象。
如果第一種假設成立,anemia輸出值就是undefined;如果第二種假設成立,則輸出值為1.
運行結果證明了第二個假設是正確的,說明函數的作用域確實在定義這個函數的時候就已經確定了
五、閉包的用途
接上個例子,
閉包的作用就是在a執行完并返回后,閉包使得Javascript的垃圾回收機制GC不會收回a所占用的資源,因為a的內部函數b的執行需要依賴a中的變量。由于閉包的存在使得函數a返回后,a中的i始終存在,這樣每次執行c(),i都是自加1后alert出i的值。
閉包有很多用途,最大的兩個好處:
(一)讀取函數內部的變量
(二)讓這些變量始終保存在內存中。
理解“(二)讓這些變量始終保存在內存中”這句話,看一下下面的額代碼。
function f1(){ var n=999; nAdd=function(){n+=1} function f2(){ alert(n); } return f2; } var result=f1(); result(); // 999 nAdd(); result(); // 1000
這段代碼中,result實際上是閉包f2函數。result一共運行了兩次,第一次是999,第二次是1000.
這說明,函數f1中的局部變量n一直保存在內存中,并沒有在f1調用后進行自動清除。
原因:f1是f2的父函數,而f2被賦給一個全局變量,這導致f2一直在內存中,f2依賴于f1,因此f1始終在內存中,不會再調用結束之后,被垃圾回收機制回收。
注意:nAdd=function(){n+=1}這個函數,首先這個函數沒有用var關鍵字,因此nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數,這個匿名函數本身就是一個閉包,可以在函數外部對函數的內部今次進行操作。
六、使用閉包的注意情況
(1)閉包會是的函數中的變量倍保存在內存中,內存消耗大,所以不能濫用閉包。否則會造成網頁性能問題,在IE中可能存在內存泄漏,解決辦法,在函數退出之前,將不使用的局部變量全部刪除。
(2)閉包會在父函數的外部,改變父函數變量內部的值。所以你把父函數當做對象object使用,把閉包當做她的公用方法,把內部變量當做他的私有屬性,不要隨便改變父函數內部變量的值。
七、JavaScript的垃圾回收機制
在JavaScript中,如果一個對象不再被引用,那么這個對象就會被GC回收。如果兩個對象相互引用,而不再被第三者所引用,那么這兩個相互引用的對象就會被回收。因為函數a被b引用,b又被a外的c引用,這就是為什么函數a執行后不會被回收的原因。
八、一些思考題
1、使用對象的閉包使用
var name="The Window"; var object={ name:"My object", getNameFunc:function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); //The Window,修改的地方
2、直接調用函數內部的函數,報錯,需要使用閉包。
function outerFun() { var a=0; function innerFun() { a++; alert(a); } } innerFun()
上面的代碼是錯誤的.innerFun()的作用域在outerFun()內部,所在outerFun()外部調用它是錯誤的.
改成下面,就可行了
function outerFun() { var a=0; function innerFun() { a++; alert(a); } return innerFun; //注意這里 } var obj=outerFun(); obj(); //結果為1 obj(); //結果為2 var obj2=outerFun(); obj2(); //結果為1 obj2(); //結果為2
3、
function outerFun() { var a =0; alert(a); } var a=4; outerFun(); //結果為0 alert(a); //結果為4
因為函數內部使用了關鍵字var維護了a的作用域在outFun()內。
function outerFun() { //沒有var a =0; alert(a); } var a=4; outerFun(); alert(a);
作用域鏈是描述一種路徑的術語,沿著該路徑可以確定變量的值 .當執行a=0時,因為沒有使用var關鍵字,因此賦值操作會沿著作用域鏈到var a=4; 并改變其值。
文章列表