上個月,淡丶無欲 讓我寫一期關於 閉包 的隨筆,其實慚愧,我對閉包也是略知一二 ,不能給出一個很好的解釋,擔心自己講不出個所以然來。 所以帶着學習的目的來寫一寫,如有錯誤,忘不吝賜教 。
為什么要有閉包?
初識閉包時,,我一直在想,為什么只有JS有閉包,c#,c++ 為什么沒有 ??
1. 封裝局部變量
看下面一個例子,計算 斐波那契 數。
為了能夠重用數據,一個通用做法就是將計算過的數據緩存起來,但緩存的數據對外是不可見的 。
看下面的 c# 代碼 :
public static class Fibonacci{
public static Fibonacci(){
cache[0] = 1;
cache[1] = 1;
}
private static IList<int> cache = new List<int>(1000,-1);
public static int Calc(n){
if(cache[n] != -1){
return cache[n];
}else{
return cache[n] = Calc(n-1) + Calc(n-2);
}
}
}
快兩年沒寫c#了, 很撇腳,囧 ,但是在這類靜態語言,這種方法很合適 。
看JS 怎么寫
var cache = [1, 1];
var calc = function(){
return cache[n] != undefined ?
cache[n]:
cache[n] = calc(n-1) + calc (n-2);
}
但是在JS中杜絕使用全局變量,所以下面改寫
var calc = function(){
return calc.cache[n] != undefined ?
calc.cache[n]:
calc.cache[n] = calc.cache(n-1) + calc.cache (n-2);
}
calc.cache = [1,1];
這里將全局變量作為 calc 的一個屬性存儲,但是對外可見,無法隱藏 。
就到了閉包大顯身手的時候,由於這里 cache 被外部調用,所以可以不被銷毀。
var Fibonacci = (function() {
var cache = [1, 1];
return {
calc: function(n) {
return cache[n] != undefined ?
cache[n] :
cache[n] = this.calc(n - 1) + this.calc(n - 2);
}
}
})();
Fibonacci.calc(5); // 8
總結:
在 c# ,c++ 等高級語言中,存在私有變量,所以無需閉包 。但是在JS中,私有變量是一件很麻煩的事情 。這時候,將局部變量放置在一個函數作用域中,可以在內部使用,而外面無法訪問。這就形成了閉包 。
2.延續對象生命周期
對於全局變量來說,全局變量的生存周期當然是永久的,除非我們主動銷毀這個全局變量。
而對於在函數內用var關鍵字聲明的局部變量來說,當退出函數時,這些局部變量即失去了它們的價值,它們都會隨着函數調用的結束而被銷毀:
現在來看看下面這段代碼:
var func = function(){
var a = 1;
return function(){
a++;
alert ( a );
}
};
var f = func();
f(); // 輸出:2
f(); // 輸出:3
f(); // 輸出:4
f(); // 輸出:5
跟我們之前的推論相反,當退出函數后,局部變量a並沒有消失,而是似乎一直在某個地方存活着。這是因為當執行 var f=func();時,f返回了一個匿名函數的引用,它可以訪問到 func()被調用時產生的環境,而局部變量 a 一直處在這個環境里。既然局部變量所在的環境還能被外界訪問,這個局部變量就有了不被銷毀的理由。在這里產生了一個閉包結構,局部變量的生命看起來被延續了。
這個特性有時候會很麻煩,但有時候很有用,用來延續局部變量的壽命 。
img 對象經常用於進行數據上報,如下所示:
var report = function( src ){
var img = new Image();
img.src = src;
};
report( 'http://xxx.com/getUserInfo' );
但是通過查詢后台的記錄我們得知,因為一些低版本瀏覽器的實現存在bug,在這些瀏覽器
下使用report函數進行數據上報會丟失30%左右的數據,也就是說,report函數並不是每一次都成功發起了 HTTP請求。 丟失數據的原因是 img 是 report 函數中的局部變量, 當 report 函數的調用結束后,img 局部變量隨即被銷毀,而此時或許還沒來得及發出 HTTP請求,所以此次請求就會丟失掉。
現在我們把 img 變量用閉包封閉起來,便能解決請求丟失的問題:
var report = (function(){
var imgs = [];
return function( src ){
var img = new Image();
imgs.push( img );
img.src = src;
}
})();
高階函數
閉包在JS中非常廣泛,常常與高階函數作伴 。
高階函數是指至少下面條件之一的函數
- 函數作為參數傳遞
- 函數作為返回值輸出
函數作為參數傳遞作為回調函數,應用場景非常廣泛,此處不再舉例 。
函數作為返回值,這里給出兩個例子 。
判斷數據類型
'use strict';
var Type = {};
for (var i = 0, type; type = ['String', 'Number', 'Boolean', 'Object'][i++];) {
(function(type) {
Type["is" + type] = function(o) {
return Object.prototype.toString.call(o) === '[object ' + type + ']';
}
})(type);
}
console.log(Type.isString("hh"));
實現單例模式
var getSingle = function(func){
var ret = null;
return function(){
return ret || ret = func.apply(this, Array.prototype.slice.call(arguments));
}
}
var getScript = getSingle(function(){
return document.createElement( 'script' );
});
var script1 = getScript();
var script2 = getScript();
alert ( script1 === script2 ); // 輸出:true
高階函數實現 AOP
AOP(面向切面編程)的主要作用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些
跟業務邏輯無關的功能通常包括日志統計、安全控制、異常處理等。把這些功能抽離出來之后 。再通過“動態織入”的方式摻入業務邏輯模塊中。這樣做的好處首先是可以保持業務邏輯模塊的純凈和高內聚性,其次是可以很方便地復用日志統計等功能模塊。
在傳統通過on = 為事件注冊處理程序中,賦值一個新的處理程序會覆蓋掉原來的處理程序,我們可以這么做
var addEvent = function(target,event ,func){
var old;
target[event] = functoin(e){
if(old = target[event]){
old();
}
func();
}
}
通常,在 JavaScript中實現 AOP,都是指把一個函數“動態植入”到另外一個函數之中,具
體的實現技術有很多,本節我們通過擴展 Function.prototype 來做到這一點
Function.prototype.before = function(beforeFn) {
var self = this;
return function() {
beforeFn.apply(this, Array.prototype.slice.call(arguments));
return self.apply(this, Array.prototype.slice.call(arguments));
}
};
Function.prototype.after = function(afterFn) {
var self = this;
return function() {
var ret;
ret = self.apply(this, Array.prototype.slice.call(arguments));
afterFn.apply(this, Array.prototype.slice.call(arguments));
return ret;
}
}
var func = function() {
console.log(2);
}
func = func.before(function() {
console.log(1);
}).after(function() {
console.log(3);
})
func();
輸出 1 2 3 。
return 后的函數中的this 取決於真實環境的this ,因為返回的是一個獨立的函數 。
高階函數的其他應用
函數柯里化
currying又稱部分求值。一個currying的函數首先會接受一些參數,接受了這些參數之后,該函數並不會立即求值,而是繼續返回另外一個函數,剛才傳入的參數在函數形成的閉包中被保存起來。待到函數被真正需要求值的時候,之前傳入的所有參數都會被一次性用於求值。
看下面一個例子
func(1); 1
func(1)(2); 2
func(1)(2)(3); 6
...
這個例子就是函數柯里化的典型應用,重點考察閉包和高階函數,也是一道比較常見的面試題
看下面的解法 。
'use strict';
var curry = (function() {
var data = [1];
var func = function(n) {
data.push(n);
return func;
}
func.valueOf = function() {
var ret = data.reduce(function(a, b) {
return a * b;
})
data = [1];
return ret;
}
return func;
})();
console.log(curry(1));
console.log(curry(1)(2));
console.log(curry(1)(2)(3));
在上面的解法中,我們將函數柯里化和數據計算放在一起,違背了單一職責原則 。現在,我們可以專門定義一個函數,用於對參數進行柯里化。
'use strict';
var curry = function(fn) {
var args = [];
var ret = function(n) {
args.push(n);
return ret;
}
ret.valueOf = function() {
var ret = args.reduce(fn);
args = [];
return ret;
}
return ret;
}
var func = curry(function(a, b) {
return a * b;
})
console.log(func(1));
console.log(func(1)(2));
console.log(func(1)(2)(3));
如果有錯誤,希望不吝賜教 ~
注: 這篇隨筆的一些例子代碼來自於 《JavaScript 設計模式與實踐 》 ,書中非常詳細並深入講解了JavaScript 高級和 17 種設計模式,對於JavaScript提高非常有幫助 。安利 ~ 。 想要電子版可以給我發郵件: mymeat@126.com
轉載請說明原文出處:http://www.cnblogs.com/likeFlyingFish/p/6421615.html