JavaScript-閉包

mazy發表於2018-07-04

什麼是閉包

在<<你不知道的JavaScript>>中解釋到: 當函式可以記住並訪問所在的詞法作用域時,就產生了閉包


變數作用域

因為閉包就是基於變數的作用域

變數的作用域

  1. 全域性變數
  2. 區域性變數

變數使用

區域性函式中使用全域性變數

var temp = 1;
function fn(){
    console.log(temp)
}
fn()
複製程式碼

js可以在區域性函式中使用全域性作用域的變數 在內部函式fn中可以使用全域性變數temp

全域性作用域中使用函式中的區域性變數

function fn(){
    var temp = 1 
}
console.log(temp)
複製程式碼
  1. 執行結果為:Uncaught ReferenceError: temp is not defined at xxx.html 報錯.
  2. 所以在函式的外部是無法使用函式中的區域性變數
  3. 注意,在區域性函式中定義變數要使用var 關鍵字,否則,會定義為全域性變數
    function fn(){
        temp = 1 
    }
    fn()
    console.log(temp)
複製程式碼

這樣就可以列印temp的值了

使用區域性變數

如果想使用函式中的區域性變數,那麼就在函式的內部在定義一個函式,並返回

function f1(){
    var temp = 'hello';
    function f2(){
        console.log(temp); 
    }
    return f2
}
f1()()
複製程式碼
  1. f2定義在f1函式的內部,所以f2函式中可以使用f1內部的區域性變數(變數查詢是一層一層往上找的)
  2. 所以當執行f2函式的時候是可以列印f1中變數temp

閉包

  1. 首先有兩個函式,為父子關係
  2. 子函式引用父函式的變數
  3. 執行函式定義就會產生閉包(不需要呼叫內部函式)

JavaScript-閉包
當一個巢狀的內部函式引用了巢狀的外部函式的變數時,就產生了閉包

演示閉包

JavaScript-閉包

閉包的生命週期

  1. 產生: 在巢狀內部函式定義執行完時就產生了閉包,而不是呼叫時(就是剛進入外部函式內部的時候就產生了閉包,因為函式提升,如果是變數接收方式定義內部函式的話,就是在執行該行程式碼時產生)
  2. 死亡: 在巢狀的內部函式成為垃圾物件時,xxx=null時

閉包的用途

  1. 可以使用函式內的區域性變數
  2. 這些變數可以始終保持在記憶體中
  3. 如果使用了單例模式,可以用到閉包
function f1() {
    var temp = 100
    function f2() {
        console.log(temp)
    }
    add = function(){
        temp += 1
    }
    return f2
}

var result = f1()
result()
add()
result()
result = null //釋放全域性變數,會銷燬常駐記憶體的區域性變數
複製程式碼
  1. 在函式f1額外定義了一個全域性回撥函式,並將temp+= 1
  2. 呼叫f2時,列印100
  3. 執行了add() temp+=1
  4. 在呼叫了f2,列印了101
  5. 如果一個物件不再被引用,那麼這個物件就會被垃圾回收機制回收
  6. 如果兩個物件互相引用,不與第三個物件產生關係,那麼這兩個物件也會被垃圾回收
  7. 當不使用的物件賦值null操作,釋放

這說明了,當f2被呼叫的時候,閉包並沒有被清除,變數仍然存在於記憶體中,給他進行+1操作,是在原變數的基礎上增加

  1. 因為閉包是父子巢狀關係,當子函式返回並用一個全域性變數接收,所以子函式會始終在記憶體中,而子函式又依賴於父函式,所以父函式也一直在記憶體中,所以不會在呼叫之後被垃圾回收機制回收

for迴圈問題

看一下下面程式碼的結果

    function test(){
    var arr = [];
    for(var i = 0;i < 10;i++){
        arr[i] = function(){
        return i;
        };
    }
    for(var a = 0;a < 10;a++){
        console.log(arr[a]());
    }
    }
    test();
複製程式碼
  1. 當列印的時候發現列印了10 個 10 ,這並不是我們想象的那種結果

但是換一種方式

function test(){
  var arr = [];
  for(let i = 0;i < 10;i++){  
    arr[i] = function(){
      return i;
    };
  }
  for(var a = 0;a < 10;a++){
    console.log(arr[a]());
  }
}
test(); 
複製程式碼
  1. 只是把for迴圈中的var 換成了let,結果就是我們想象的那樣了(0,1,2,...9)

這是為什麼

  1. 這個跟var 和let 的塊作用域有關,在es5中,for迴圈中的var並不代表著一個塊作用域,所以for迴圈中用var定義的是一個全域性變數,或者是for迴圈外函式的區域性變數,當迴圈結束,變數的值自然而然的變成了10
  2. 但是let就不一樣了,也會將for作為一個塊作用域(其實是離let最近的{},let可以繫結到任意塊作用域上),也就是說for迴圈內部與外部不在同一個作用域內

建議

  1. 正常函式的作用域或者其他變數的都會在函式執行之後被銷燬,但是建立了閉包會使區域性變數長期儲存在記憶體中,消耗記憶體很大,所以不能濫用,容易導致內訓洩露或效能問題,但是可以在退出函式時將不使用的區域性變數刪除(將 f1 = null... 處理)
  2. 閉包會在父函式外部,改變內部變數的值,所以存在操作失誤的可能

阮大神的兩個思考題

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()());
複製程式碼

解析

  1. 閉包中引用了this指標
  2. 因為閉包會賦值給全域性變數,所以那時this指向的其實是window物件
var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name;
        };
    }
};
alert(object.getNameFunc()());
複製程式碼

解析

  1. 跟上面的區別就是將this賦值給了區域性變數,
  2. 因為閉包,所以只有在函式內部可以使用,當呼叫的的時候依然是函式內部的變數

一道經典的面試題

    function fun(n,o){
        console.log(o);
        return {
            fun: function(m){
                return fun(m,n);
            }
        };
    }
    //n = 0 o為undefined
    var a = fun(0);  
    //  m = 1 n = 0 o為0
    a.fun(1);  
    //n沒有變化 依然是0  
    a.fun(2);
    //n沒有變化 依然是0  
    a.fun(3);
    //undefined,0,1,2
    //這是個難點:當fun(0) => n=0,o=undefined,當.fun(1)=> n=0,m=1,o=0,.fun(2)=> m=2,n=1,o=1,...
    var b = fun(0).fun(1).fun(2).fun(3); 
    //undefined,0
    var c = fun(0).fun(1); 
    //1
    c.fun(2); 
    c.fun(3); 
複製程式碼

相關文章