JS學習理解之閉包和高階函數

1、閉包

對於 JavaScript 程序員來講,閉包(closure)是一個難懂又必須征服的概念。閉包的造成與
變量的做用域以及變量的生存週期密切相關。下面咱們先簡單瞭解這兩個知識點。javascript

1.1 變量的做用域

變量的做用域,就是指變量的有效範圍。咱們最常談到的是在函數中聲明的變量做用域。java

當在函數中聲明一個變量的時候,若是該變量前面沒有帶上關鍵字 var,這個變量就會成爲 全局變量,這固然是一種容易形成命名衝突的作法。
另一種狀況是用 var 關鍵字在函數中聲明變量,這時候的變量便是局部變量,只有在該函 數內部才能訪問到這個變量,在函數外面是訪問不到的。代碼以下:程序員

var func = function(){ var a = 1;
    alert ( a ); // 輸出: 1 
};
func();
alert ( a ); // 輸出:Uncaught ReferenceError: a is not defined

在 JavaScript 中,函數能夠用來創造函數做用域。此時的函數像一層半透明的玻璃,在函數 裏面能夠看到外面的變量,而在函數外面則沒法看到函數裏面的變量。這是由於當在函數中搜索 一個變量的時候,若是該函數內並無聲明這個變量,那麼這次搜索的過程會隨着代碼執行環境 建立的做用域鏈往外層逐層搜索,一直搜索到全局對象爲止。變量的搜索是從內到外而非從外到 內的。ajax

下面這段包含了嵌套函數的代碼,也許能幫助咱們加深對變量搜索過程的理解:編程

var a = 1;
var func1 = function(){ 
    var b = 2;
    var func2 = function(){ 
        var c = 3;
        alert ( b ); // 輸出:2
        alert ( a ); // 輸出:1
    }
    func2(); 
    alert ( c );// 輸出:Uncaught ReferenceError: c is not defined
}; 
func1();

1.2 變量的生存週期

除了變量的做用域以外,另一個跟閉包有關的概念是變量的生存週期。設計模式

對於全局變量來講,全局變量的生存週期固然是永久的,除非咱們主動銷燬這個全局變量。數組

而對於在函數內用 var 關鍵字聲明的局部變量來講,當退出函數時,這些局部變量即失去了 它們的價值,它們都會隨着函數調用的結束而被銷燬:瀏覽器

var func = function(){
var a = 1; // 退出函數後局部變量 a 將被銷燬 alert ( a );
}; func();

如今來看看下面這段代碼:緩存

var func = function(){ 
    var a = 1;
    return function(){ 
        a++;
        alert ( a );
    } 
};
var f = func();
f(); // 輸出:2
f(); // 輸出:3
f(); // 輸出:4
f(); // 輸出:5

1.3閉包的做用

1.3.1 封裝變量

閉包能夠幫助把一些不須要暴露在全局的變量封裝成「私有變量」。假設有一個計算乘積的
簡單函數:閉包

var mult = function(){ var a = 1;
for ( var a = a
}
return a; };
i = 0, l = arguments.length; i < l; i++ ){ * arguments[i];

mult 函數接受一些 number 類型的參數,並返回這些參數的乘積。如今咱們以爲對於那些相同 的參數來講,每次都進行計算是一種浪費,咱們能夠加入緩存機制來提升這個函數的性能:

var cache = {};
var mult = function(){
    var args = Array.prototype.join.call( arguments, ',' ); 
    if ( cache[ args ] ){
        return cache[ args ]; 
    }
    var a = 1;
    for ( var i = 0, l = arguments.length; i < l; i++ ){
        a = a * arguments[i]; 
    }
    return cache[ args ] = a; 
};
alert ( mult( 1,2,3 ) ); // 輸出:6
alert ( mult( 1,2,3 ) ); // 輸出:6

咱們看到 cache 這個變量僅僅在 mult 函數中被使用,與其讓 cache 變量跟 mult 函數一塊兒平行 地暴露在全局做用域下,不如把它封閉在 mult 函數內部,這樣能夠減小頁面中的全局變量,以 4 避免這個變量在其餘地方被不當心修改而引起錯誤。代碼以下:

var mult = (function(){
    var cache = {}; 
    return function(){
        var args = Array.prototype.join.call( arguments, ',' ); 
        if ( args in cache ){
            return cache[ args ]; 
        }
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i]; 
        }
        return cache[ args ] = a; 
    }
})();

提煉函數是代碼重構中的一種常見技巧。若是在一個大函數中有一些代碼塊可以獨立出來, 咱們經常把這些代碼塊封裝在獨立的小函數裏面。獨立出來的小函數有助於代碼複用,若是這些 小函數有一個良好的命名,它們自己也起到了註釋的做用。若是這些小函數不須要在程序的其餘 9 地方使用,最好是把它們用閉包封閉起來。代碼以下:

var cache = {};
var mult = (function(){
    var cache = {};
    var calculate = function(){ // 封閉 calculate 函數
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i];
        }
        return a;
    };
    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if ( args in cache ){
            return cache[ args ];
        }
        return cache[ args ] = calculate.apply( null, arguments );
    }
})();

1.3.2 延續局部變量的壽命

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;
    }
})();

2、高階函數

高階函數是指至少知足下列條件之一的函數。

  • 函數能夠做爲參數被傳遞;
  • 函數能夠做爲返回值輸出。

JavaScript 語言中的函數顯然知足高階函數的條件,在實際開發中,不管是將函數看成參數
傳遞,仍是讓函數的執行結果返回另一個函數,這兩種情形都有不少應用場景,下面就列舉一
些高階函數的應用場景。

2.1 函數做爲參數傳遞

把函數看成參數傳遞,這表明咱們能夠抽離出一部分容易變化的業務邏輯,把這部分業務邏
輯放在函數參數中,這樣一來能夠分離業務代碼中變化與不變的部分。其中一個重要應用場景就
是常見的回調函數。

1. 回調函數

在 ajax 異步請求的應用中,回調函數的使用很是頻繁。當咱們想在 ajax 請求返回以後作一
些事情,但又並不知道請求返回的確切時間時,最多見的方案就是把 callback 函數看成參數傳入
發起 ajax 請求的方法中,待請求完成以後執行 callback 函數:

var getUserInfo = function( userId, callback ){
    $.ajax( 'http://xxx.com/getUserInfo?' + userId, function( data ){
        if ( typeof callback === 'function' ){
            callback( data );
        }
    });
}
getUserInfo( 13157, function( data ){
    alert ( data.userName );
});

回調函數的應用不只只在異步請求中,當一個函數不適合執行一些請求時,咱們也能夠把這些請求封裝成一個函數,並把它做爲參數傳遞給另一個函數,「委託」給另一個函數來執行。

2. Array.prototype.sort

Array.prototype.sort 接受一個函數看成參數,這個函數裏面封裝了數組元素的排序規則。從Array.prototype.sort 的使用能夠看到,咱們的目的是對數組進行排序,這是不變的部分;而使用 什 麼 規 則 去 排 序 , 則 是 可 變 的 部 分 。 把 可 變 的 部 分 封 裝 在 函 數 參 數 裏 , 動 態 傳 入Array.prototype.sort,使 Array.prototype.sort 方法成爲了一個很是靈活的方法,代碼以下:

//從小到大排列
[ 1, 4, 3 ].sort( function( a, b ){
    return a - b;
});
// 輸出: [ 1, 3, 4 ]

//從大到小排列
[ 1, 4, 3 ].sort( function( a, b ){
    return b - a;
});
// 輸出: [ 4, 3, 1 ]

2.2 函數做爲返回值輸出

相比把函數看成參數傳遞,函數看成返回值輸出的應用場景也許更多,也更能體現函數式編程的巧妙。讓函數繼續返回一個可執行的函數,意味着運算過程是可延續的。

1. 判斷數據的類型

咱們來看看這個例子,判斷一個數據是不是數組,在以往的實現中,能夠基於鴨子類型的概念來判斷,好比判斷這個數據有沒有 length 屬性,有沒有 sort 方法或者 slice 方法等。但更好的方式是用 Object.prototype.toString 來計算。 Object.prototype.toString.call( obj )返回一個字 符 串 , 比 如 Object.prototype.toString.call( [1,2,3] ) 總 是 返 回 "[object Array]" , 而Object.prototype.toString.call( 「str」)老是返回"[object String]"。因此咱們能夠編寫一系列的isType 函數。代碼以下:

var isString = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object String]';
};
var isArray = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Array]';
};
var isNumber = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Number]';
};

咱們發現,這些函數的大部分實現都是相同的,不一樣的只是 Object.prototype.toString.call( obj )返回的字符串。爲了不多餘的代碼,咱們嘗試把這些字符串做爲參數提早值入 isType函數。代碼以下:

var isType = function( type ){
    return function( obj ){
        return Object.prototype.toString.call( obj ) === '[object '+ type +']';
    }
};
var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumber = isType( 'Number' );
console.log( isArray( [ 1, 2, 3 ] ) ); // 輸出: true

咱們還能夠用循環語句,來批量註冊這些 isType 函數:

var Type = {};
for ( var i = 0, type; type = [ 'String', 'Array', 'Number' ][ i++ ]; ){
    (function( type ){
        Type[ 'is' + type ] = function( obj ){
            return Object.prototype.toString.call( obj ) === '[object '+ type +']';
        }
    })( type )
};
Type.isArray( [] ); // 輸出: true
Type.isString( "str" ); // 輸出: true

2. getSingle

下面是一個單例模式的例子,在第三部分設計模式的學習中,咱們將進行更深刻的講解,這
裏暫且只瞭解其代碼實現:

var getSingle = function ( fn ) {
    var ret;
    return function () {
        return ret || ( ret = fn.apply( this, arguments ) );
    };
};

這個高階函數的例子,既把函數看成參數傳遞,又讓函數執行後返回了另一個函數。咱們能夠看看 getSingle 函數的效果:

var getScript = getSingle(function(){
`return document.createElement( 'script' );
});
var script1 = getScript();
var script2 = getScript();
alert ( script1 === script2 ); // 輸出: true

注:內容摘取《Javascript設計模式與開發實踐》

相關文章
相關標籤/搜索