白話js閉包

開始閉包的話題

閉包是js的一個語言特性,談起它首先要從做用域出發。面試

咱們從最簡單的角度來說做用域:bash

<script>
    var a = 1;
    function echoA(){
        console.log(a);
    }
    echoA();        //1
    function echoA_again(){
        var a = 2;
        var b = 3;
        console.log(a);
    }
    eachA_again();      //2
    console.log(b);     //undefine
</script>
複製代碼

好了,能夠看到第一個a的做用域是全局,而在echoA_again()裏又聲明一個a,此時這個a的做用域僅存在於該函數中,並且從console.log(b)中咱們能夠發現,做用域內部定義的變量,做用域外沒法訪問。閉包

好了,那麼問題開始,再沒有CommonJS、AMD和ES6 module的年代,開發者如何才能避免本身寫的變量名或者方法名不會和團隊裏的代碼衝突呢?好比小王寫了個方法叫generateID用於生成一個數字ID,而小張寫了一個方法一樣叫generateID用於生成一個字符串ID,若是不能好好處理,那麼後引入的一個就會把前一個覆蓋。這個問題的解決方案就是,閉包異步

從概念上理解閉包

首先,閉包是一個函數。函數

其次,這個函數返回一個做用域內的變量。ui

由於JavaScript中函數是一等公民。編碼

因此使用閉包的狀況至關於:「一個函數返回另外一個((帶着該函數做用域內的變量的)函數)es5

例子:spa

function A(){
	var b = 40;
	function myClosure(){
		return b;
	}
	return myClosure;
}
複製代碼

上例中,myClosure就是閉包,它做爲一個函數返回一個做用域內的變量b,所以當執行A返回myClosure這個函數時,天然而然的就能讓外界獲取b的值了。code

特性

首先,閉包這一函數返回的是內部做用域的變量,由於內部做用域的變量不能被外部訪問,所以第一點特性就是,閉包能夠暴露內部做用域的變量

再次,變量若是再也不被使用就會被回收,內部做用域變量用完會被回收的,可是如今這個閉包函數一直在引用着這個變量,所以該閉包函數不被回收的狀況下該變量不會被回收,這就是第二點特性,閉包能夠防止變量被回收

缺點

上述特性做爲優勢存在也會帶來必定的問題

閉包由於會防止變量被回收,從而致使環境中的變量沒法獲得釋放,不正確的使用可能會帶來內存的泄露問題。注意,是不正確的使用!而不是"絕對"會引發內存泄漏!!!

缺點的解決方案

如特性中第二條所說,在閉包函數不被回收的狀況下該變量不會被回收,所以,引用閉包函數的做用域若是被銷燬,則閉包所在的做用域會被完全回收。所以,編碼中須要特別注意閉包的生命週期。

用途

隔離代碼

在上古時期,在沒有CommonJS、AMD和ES6 module的年代,你要怎麼去寫一個js模塊或者函數庫呢?比方說咱們要寫一個數學運算庫

<script>
    var header = 'result is '
    function add(number1,number2){
        return header + (number1 + number2);
    }
</script>
複製代碼

而後你把他引入頁面了,ok,暫時沒問題,你經過使用 add(1,2)正確的得到了'result is 3'

可是有一天你的同事在本身的代碼裏定義了

<script>
     function add(number1){
         return function(number2){
             return number1 + number2;
         }
     }
</script>
複製代碼

他經過調用add(1)(2)正確的返回了3,可是你的方法卻被覆蓋了,由於大家倆的方法都是在全局定義的。怎麼解決?

用對象?

<script>
    var header = 'result is ';
    var myMath = {
        add:  function add(number1,number2){
        return header + (number1 + number2);
        }
    }
</script>
複製代碼

而後你調用myMath.add(1,2)正確返回了3,並且你的同事直接修改add也不會影響你了,等等,他修改header怎麼辦?header是你的模塊內部的變量,怎麼能夠給暴露出去,多危險啊。

因此你須要閉包,

<script>
    var myMath = (function(){
        var header = 'result is ';
        function add(number1,number2){
            return header + (number1 + number2);
        }
        return {add:add}
    })();
</script>
複製代碼

ok,任何人只有覆蓋掉你的myMath才能夠對你的代碼產生影響!就是這麼好用。

固然你想說用class(es5實現class),可是你要知道模塊的定義屬於namespace,namespace是包括class的,何況有的狀況你不須要每次都執行個new 吧。

誤區

閉包和當即執行函數傻傻分不清

面試中出現頻率最高的問題就是setTimeout問題

for(var i = 0; i < 5 ; i++){
        setTimeout(()=>console.log(i),1000);
    }
    
    //返回 5 5 5 5 5 
複製代碼

你覺得執行的順序是

i = 0  console.log(i) i++
    i = 1  console.log(i) i++
    i = 2  console.log(i) i++
    i = 3  console.log(i) i++
    i = 4  console.log(i)
複製代碼

可是因爲js的運行機制,他的實際運行是

i = 0 i++
    i = 1 i++
    i = 2 i++
    i = 3 i++
    i = 4
    console.log(i)
    console.log(i)
    console.log(i)
    console.log(i)
    console.log(i)
複製代碼

由於settimeout做爲異步函數他的執行順序(回調順序)是在全部同步任務執行完後執行,所以咱們須要在本次事件循環中把異步包裝成同步,而且要把變量封裝近內部,所以代碼以下

for(var i = 0; i < 5; i++){
        (function(i){
            setTimeout(()=>console.log(i),1000);
        })(i);
    }
複製代碼

在這裏 function(i){這一部分把每次的i變化保存,而後傳遞給setTimeout,這個事件是同步的,而setTimeout的異步執行屬於匿名函數內的事件循環,因此for中的i變化不會致使匿名函數內部引用的i變化。它與咱們上述的閉包定義徹底不一樣。(這裏就涉及到js的事件循環了,須要能夠去補一補這個知識)。

另外,當即執行函就是一個簡寫的方法,

(function(){
     //do something
 })()
 
 //至關於
 
 function foo(){
     //do something
 }
 foo();
複製代碼

僅此而已。

相關文章
相關標籤/搜索