設定模式基礎 之 3閉包

zhaoyezi發表於2018-05-28

閉包的形成與變數的作用域以及變數的生存週期密切相關。

1. 什麼是閉包?

  • 閉包就是函式的區域性變數集合,只是這些區域性變數在函式返回結果後依然存在
  • 閉包就是函式的堆疊在函式返回以後並不釋放,我們可以理解這些函式堆疊並不是在棧上分配而是在堆上分配。
  • 當一個函式內部定義另外一個函式就會產生閉包 一句話總結:指代有許可權訪問另外一個函式作用域中的變數的函式。

2. 作用域

變數的作用域有兩種,全域性變數和區域性變數。javascript語言的特殊之處就是:

  • 處於函式內部可以直接讀取全域性變數。
  • 處於函式外部無法讀取到函式內部的區域性變數。 變數定義規則:
  • 變數宣告後,會被新增到所處位置最近的環境中。
  • 沒有使用var 定義的變數,會直接新增到全域性環境。
  • 沒有使用var進行宣告的變數,可以使用delete進行刪除

變數提升與函式提升

  • 將變數替身到函式頂部(只是提升宣告,不會提升值).
  • 將函式提升到函式頂部(函式建立三種方式: 函式宣告,函式表示式,建構函式,只有函式宣告方式能函式提升)

// 變數提升
var a = "global";
(function() {
    console.log(a); // undefined. 由自己的作用域查詢開始,沒找到再往外部作用域查詢。在函式作用域的變數物件中(varaible object)a變數,只是還沒有被賦值。所以為undefined.
    var a = "part";
})();

// 函式提升
function external() {
    internal(); // internal , 函式提升
    console.log(internalVariable); // undefined, 變數提升
    console.log(internalFunc); // undefined, 變數提升
    function internal() {
        console.log("internal");
    }
    var internalVariable = "internalVariable";
    var internalFunc = function () {
        console.log("internalFunc");
    }
}
複製程式碼

3 閉包

開發中,處於某些原因,我們有時候需要得到函式內部的區域性變數,在上面的作用域解釋中,外部是不能訪問內部變數的,因此我們可以通過閉包實現。

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
        lert(n);
    }   
return f2;
}
var result=f1();
result();// 彈出999
複製程式碼

在這段程式碼中,result實際上就是閉包f2函式。它一共執行了兩次,第一次的值是999,第二次的值是1000。這證明了,函式f1中的區域性變數n一直儲存在記憶體中,並沒有在f1呼叫後被自動清除。 為什麼會這樣呢?原因就在於f1是f2的父函式,而f2被賦給了一個全域性變數,這導致f2始終在記憶體中,而f2的存在依賴於f1,因此f1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。 這段程式碼中另一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。

3.1 注意點

  • 由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。
  • 閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。

3.2 閉包案例

案例1

來源:http://www.cnblogs.com/zichi/p/5092997.html

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
複製程式碼

先看第一組執行,fun(0) 後首先列印 undefined,沒有問題。之後變數 a 便被賦值為 fun 函式所 return 的物件。這裡要重點注意的是引數 n,值為 0,這就是閉包和作用域鏈

a = {
    fun: function(m) {
        // 這裡的n替換為了外層傳入的n值
        return fun(m, 0);
    }
};
複製程式碼

接著執行 a.fun(1) a.fun(2) a.fun(3),我們以 a.fun(1) 舉例。a.fun(1) 的執行結果,因為沒有賦值(其實有個 return value),所以其實就是執行了一遍 fun(m, n),上面說了,n 值為 0,所以控制檯輸出為 0。後兩個輸出同理。這裡要注意的就是這個 n,因為作用域鏈,所以 n 能獲取值,為 0,因為 n 被變數 a 所引用,所以它一直貯藏在記憶體中。

第二組,我們拆分了來看,實際可以修改為如下:

var a = fun(0);
var b = a.fun(1); 
var c = b.fun(2);
var d = c.fun(3); 
複製程式碼

第一行,列印 undefined,沒有問題,a 返回物件,然後執行 a.fun(1),列印 0,這些跟第一次的執行相同。a.fun(1),其實就是執行 fun(m, n),其實就是 fun(1, 0),return 的物件賦值給 b。

var b = {
  fun: function(m) {
    return fun(m, n); // n=1
  }
};
複製程式碼

類似的結果,唯一不同的是 n 的值變了,這是由 fun() 傳入的引數所決定的。接下去,b.fun(2),執行 fun(2, 1),列印出 1,然後將 return 的物件賦值給 c。

var c = {
  fun: function(m) {
    return fun(m, n); // n=2
  }
};
複製程式碼

最後一步也是類似,所以依次列印 undefined, 0, 1, 2。 第三組的與第二組沒什麼大的區別,列印結果: undefined, 0, 1, 1

個人認為這道題的 "噁心" 之處多數在於函式中呼叫函式本身(fun 函式中呼叫 fun 函式),而引起的思路混亂,其他部分其實跟下面程式碼類似,歸根結底就是被引用的變數會始終存在在記憶體中。

案例2 - 計算乘積的簡單函式

閉包可以幫助把一些不需要暴露在全域性的變數封裝成私有變數

  • 設計為閉包方式,cache為函式內部區域性變數,外部不能訪問
  • 將計算規則提取為一個方法calculate
  • 返回一個匿名函式
var mult = (function() {
    var cache = {};
    var calculate = function() {
        console.log(arguments);
        var temp = 1;
        [].forEach.call(arguments, function(value) {
            temp *=  value;
        })
        return temp;
    }
    return function() {
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
            return cache[args];
        }
        console.log(arguments);
        return cache[args] = calculate.apply(null, arguments); // 等同於cache[args] = calculate(...arguments)
    }
})();
mult(1, 2, 3, 4);
複製程式碼

案例3 - 包裝一個物件

過程與資料的結合是形容物件導向中的“物件”時經常使用的表達。物件以方法的形式包含了過程,而閉包則是在過程中以環境的形式包含了資料。通常用物件導向思想能實現的功能,用閉包也能實現。

var user = {
    name: 'yezi',
    sayHello: function(content) {
        console.log(this.name += content);
    }
};
user.sayHello('hello')
複製程式碼

下面我們使用閉包的方式來完成上述功能:

var User = function() {
    var name = "yezi";
    return {
        sayHello: function(content) {
            console.log(name += content);
        }
    }
};
var user = User();
user.sayHello('hello');
複製程式碼

案例4 - 開關電視

使用物件的方式,來對電視實現開關的命令執行操作。

var TV = {
    open: function() {
        console.log('open tv');
    },
    close: function() {
        console.log('close tv');
    }
};
var OpenCommand = function(receiver) {
    this.receiver = receiver;
}
OpenCommand.prototype.excute = function() {
    this.receiver.open();
};
OpenCommand.prototype.undo = function() {
    this.receiver.close();
};

var command = new OpenCommand(TV);
command.excute(); // open tv
command.undo(); // close tv
複製程式碼

使用閉包的方式完成:

var TV = {
    open: function() {
        console.log('open tv');
    },
    close: function() {
        console.log('close tv');
    }
};
var openCommand = function(reciever) {
    function excute() {
        reciever.open();
    }
    function undo() {
        reciever.close();
    }
    return {
        excute: excute,
        undo: undo
    };
}
var command = openCommand(TV);
command.excute(); // open tv
command.undo(); // close tv
複製程式碼

相關文章