JavaScript中閉包的本質

閉包是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. 作用域鏈的理解

  1. 可以把作用域鏈理解成是一個棧結構;
  2. 每個作用域都有一個作用域對象用于保存在該作用域內創建的變量(包括函數),其保存的方式是:在作用域內創建的變量會成為作用域對象的屬性;
  3. 作用鏈鏈保存的是各級作用域對象的引用,其中最近的作用域的作用域對象在最前端,越遠的作用域的作用域對象越靠后;
  4. 全局作用域的作用域對象是全局對象本身;所以,每個作用域鏈的最后端都是全局對象的引用;
  5. 在全局作用域內創建的變量會成為全局對象的屬性的原因:由于2(在作用域內創建的變量會成為作用域對象的屬性)和4(全局作用域的作用域對象是全局對象本身),所以在全局作用域創建的變量會成為全局對象的屬性;
  6. 函數的作用域鏈是在函數對象被創建時(被定義時)創建的;
  7. 每當函數被執行時,都會新創建一個函數的作用域對象,并把該作用域對象推到作用域鏈的最前端;
  8. 每當函數執行結束時,都會把函數的作用域對象從該函數作用鏈中推出;

3. 閉包的本質

其實閉包攜帶外部變量的機制并非閉包的特有機制,它是函數的作用域鏈的一個效應;在JavaScript中,閉包和普通函數沒有任何本質的區別,閉包只是函數在某種使用場景下的一個名字,就好比兇器只是刀在用于行兇時的名字;

JavaScript中的閉包能攜帶外部變量的原因是:
JavaScript的函數在被創建時(被定義時)會生成自己的作用域鏈;該作用域鏈會保存各級作用域對象的引用,所以JavaScript的函數能夠訪問其外部的所有變量;
詳見上文的< 作用域鏈的理解 >

所以,本質上,JavaScript中的閉包攜帶的不是外部變量,而是外部的作用域對象;

4. 使用閉包的建議

由于JavaScript中的函數(包括閉包)會創建并攜帶外部的作用域鏈;所以,建議:

  1. 閉包的嵌套不要太深;
    閉包嵌套越深,占用的內存空間就越大;
  2. 不要使用過多的閉包;因為閉包較占內存;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容