1 什麼是閉包
1.1 作用域鏈
- 函數的執行依賴於變量作用域,這個作用域是在創建函數定義是決定的,而不是在調用時決定的。
要理解閉包,就必須先了解作用域鏈。看代碼
function f1() {
var n = 999;
function f2() {
var m = 1;
alert(n); //999
}
alert(m); //error:無法訪問變量m。
}
從上面代碼我們可以看出,f2可以訪問f1內局部變量,但反過來不行。
1.2 從外部獲取函數局部變量
還是上面的函數,如果我們希望從外部獲取f1函數的變量n,就像在f1中獲取f2的局部變量m一樣不可行。但是f2何以獲取n,我們利用這一點來獲取變量n。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
f1()(); //999
1.3 閉包的概念
閉包概念:
閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
其他理解:
通過1.2我們瞭解如何從函數外訪問函數內的變量,例子中的f2就是閉包。閉包連接了函數外與函數的作用域。所以在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。有權訪問另一個函數作用域內變量的函數都是閉包
個人理解:
我們也可以通過”閉包”這兩個字面來理解。拿上面的代碼來說,有人會提出質疑:既然在javascript中函數也是對象,那執行f1()返回的f2函數也只是簡單的將f2函數當作對象返回。這種質疑沒錯,f1函數中確實將f2函數當作對象返回,但重要的時,f1的return同時將內部作用域也一起返回出來,而並不是僅僅返回f2對象。如此說來,”包”這個字就好理解了:將作用域及f2”打包”返回。而”閉”則代表封閉的意思,也就是f1函數的作用域封閉。
2 閉包的作用
總結起來,閉包有倆作用:
1. 可在外部讀取函數內部的變量。
2. 讓閉包所佔用的資源始終保存在內存中,不會被垃圾回收機制GC回收。。
function f1() {
var n = 999;
nAdd = function () {
n += 1
}
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
3 Javascript的垃圾回收機制
在Javascript中,如果一個對象不再被引用,那麼這個對象就會被GC回收。如果兩個對象互相引用,而不再被第3者所引用,那麼這兩個互相引用的對象也會被回收。
4 詳解閉包
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();
如果要更加深入的瞭解閉包以及函數a和嵌套函數b的關係,我們需要引入另外幾個概念:
函數的執行環境(excution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數a從定義到執行的過程爲例闡述這幾個概念。
- 當定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)設置爲定義a時a所在的“環境”,如果a是一個全局函數,則scope chain中只有window對象。
- 當執行函數a的時候,a會進入相應的執行環境(excution context)。
- 在創建執行環境的過程中,首先會爲a添加一個scope屬性,即a的作用域,其值就爲第1步中的scope chain。即a.scope=a的作用域鏈。
- 然後執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型而且不能通過JavaScript代碼直接訪問。創建完活動對象後,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。
- 下一步是在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
- 最後把所有函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,因此如同第3步,函數b的作用域鏈被設置爲b所被定義的環境,即a的作用域。
到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的作用域鏈包含了對函數a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數。函數b被c引用,函數b又依賴函數a,因此函數a在返回後不會被GC回收。
5 閉包的應用場景
- 保護函數內的變量安全。
- 在內存中維持一個變量。
- 通過保護變量的安全實現JS私有屬性和私有方法(不能被外部訪問)。
var counter = function () {
var n = 0;
return {
count: function () { return n++; },
reset: function () { n = 0; }
}
}
var c = counter(), d = counter();
c.count(); //0
d.count(); //0
c.reset();
c.count(); //1
d.count(); //0
每次調用counter()都會創建一個新的作用域鏈和一個新的私有變量,所以c和d的私有變量互不干擾。
6 通過閉包實現getter和setter
function counter(n){
return {
get count() { return n++; },
set count(m){
n = m;
}
};
}
var a = counter(100);
console.log(a.count); //100
a.count = 200;
console.log(a.count); //200
- get set關鍵字爲ES5添加,支持IE9+。
7 閉包出現的問題
- 由於閉包將變量式中保存在內存中,可能會導致內存溢出。
- 閉包可以訪問函數內的局部變量,所以可能會錯誤的修改局部變量。
- 不希望共享的變量共享了其他的閉包:
function constfuncs(){
var funcs = [];
for(var i=0; i<10; i++){
funcs[i] = function(){ return i; };
}
return funcs;
}
var funcs = constfuncs();
funcs[5](); //結果是?
上面函數利用for循環創建了多個閉包,但這些閉包內返回的是constcuncs函數內的局部變量i。其實只有一個變量i,每一個閉包返回的都是這個變量i。最後i爲10跳出循環,所以funcs5的結果是10。