閉包是JavaScript開發人員常常談論的問題,大家普遍對閉包的認知如下:
模糊的認知:閉包是定義在函數內部的函數;
清晰的認知:閉包是會保存它引用到的外部變量的特殊函數;
其實在JavaScript語言中,以上2種認知都是錯誤的;為了幫助大家正確地認識閉包,現分享出我對閉包的研究和理解,如下:(若想更深入地理解JavaScript的各種特性,可以參考另一篇文章:《JavaScript的發現與理解》)
1. 閉包
我對閉包的定義是:
閉包的標準定義:攜帶外部變量的函數稱為閉包;
我之所以這樣對閉包下定義,是因為這個定義幾乎適用所有語言的閉包,如:Object-C、Swift、JavaScript等等;所以我認為這是較標準的定義;
對于JavaScript中的閉包雖然符合標準定義,但是由于JavaScript語言的一些特性,使得JavaScript中的閉包的實現與其它語言(如:Object-C、Swift)的實現并不一樣;
很多人都認為JavaScript中的閉包只會攜帶它內部引用的外部變量,并不會攜帶沒有引用的外部變量,其實這是錯誤的;可以通過下面的代碼證明:
function outFun() {
var outArg1 = "外部參數1";
var outArg2 = "外部參數2";
function outArg3() {
console.log("外部參數3");
}
/*定義閉包
* codeStr:字符串類型的參數,該參數的值將被當作代碼執行
* return : 返回將codeStr作為代碼執行的結果;
* */
function closureFun(codeStr) {
console.log("閉包引用的變量的值:",outArg1);
return eval(codeStr); //返回將codeStr作為代碼執行的結果;
}
return closureFun;
}
var getValueOf = outFun(); //獲取閉包
var arg2Value = getValueOf("outArg2"); //嘗試獲取閉包內沒有引用的變量outArg2的值;
console.log(arg2Value); //輸出結果為:外部參數2
var arg3Value = getValueOf("outArg3"); //嘗試獲取閉包內沒有引用的函數outArg3;
arg3Value(); //輸出結果為:外部參數3
從示例代碼中的運行結果中可以看出,對于閉包引用到的外部變量outArg1 和 閉包沒有引用到的變量outArg2和函數outArg3,在閉包執行時都能被正確地訪問到,所以閉包會攜帶所有的外部變量(函數也是變量);
為什么會這樣呢?若要理解,還需先了解一下作用域鏈的知識,下面是我對JavaScript的作用域鏈的理解:
2. 作用域鏈的理解
- 可以把作用域鏈理解成是一個棧結構;
- 每個作用域都有一個作用域對象用于保存在該作用域內創建的變量(包括函數),其保存的方式是:在作用域內創建的變量會成為作用域對象的屬性;
- 作用鏈鏈保存的是各級作用域對象的引用,其中最近的作用域的作用域對象在最前端,越遠的作用域的作用域對象越靠后;
- 全局作用域的作用域對象是全局對象本身;所以,每個作用域鏈的最后端都是全局對象的引用;
- 在全局作用域內創建的變量會成為全局對象的屬性的原因:由于2(在作用域內創建的變量會成為作用域對象的屬性)和4(全局作用域的作用域對象是全局對象本身),所以在全局作用域創建的變量會成為全局對象的屬性;
- 函數的作用域鏈是在函數對象被創建時(被定義時)創建的;
- 每當函數被執行時,都會新創建一個函數的作用域對象,并把該作用域對象推到作用域鏈的最前端;
- 每當函數執行結束時,都會把函數的作用域對象從該函數作用鏈中推出;
3. 閉包的本質
其實閉包攜帶外部變量的機制并非閉包的特有機制,它是函數的作用域鏈的一個效應;在JavaScript中,閉包和普通函數沒有任何本質的區別,閉包只是函數在某種使用場景下的一個名字,就好比兇器只是刀在用于行兇時的名字;
JavaScript中的閉包能攜帶外部變量的原因是:
JavaScript的函數在被創建時(被定義時)會生成自己的作用域鏈;該作用域鏈會保存各級作用域對象的引用,所以JavaScript的函數能夠訪問其外部的所有變量;
詳見上文的< 作用域鏈的理解 >
所以,本質上,JavaScript中的閉包攜帶的不是外部變量,而是外部的作用域對象;
4. 使用閉包的建議
由于JavaScript中的函數(包括閉包)會創建并攜帶外部的作用域鏈;所以,建議:
- 閉包的嵌套不要太深;
閉包嵌套越深,占用的內存空間就越大;- 不要使用過多的閉包;因為閉包較占內存;