JS學(xué)習(xí)理解之閉包和高階函數(shù)

一、閉包

對(duì)于 JavaScript 程序員來說,閉包(closure)是一個(gè)難懂又必須征服的概念。閉包的形成與
變量的作用域以及變量的生存周期密切相關(guān)。下面我們先簡(jiǎn)單了解這兩個(gè)知識(shí)點(diǎn)。

1.1 變量的作用域

變量的作用域,就是指變量的有效范圍。我們最常談到的是在函數(shù)中聲明的變量作用域。

當(dāng)在函數(shù)中聲明一個(gè)變量的時(shí)候,如果該變量前面沒有帶上關(guān)鍵字 var,這個(gè)變量就會(huì)成為 全局變量,這當(dāng)然是一種容易造成命名沖突的做法。
另外一種情況是用 var 關(guān)鍵字在函數(shù)中聲明變量,這時(shí)候的變量即是局部變量,只有在該函 數(shù)內(nèi)部才能訪問到這個(gè)變量,在函數(shù)外面是訪問不到的。代碼如下:

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

在 JavaScript 中,函數(shù)可以用來創(chuàng)造函數(shù)作用域。此時(shí)的函數(shù)像一層半透明的玻璃,在函數(shù) 里面可以看到外面的變量,而在函數(shù)外面則無法看到函數(shù)里面的變量。這是因?yàn)楫?dāng)在函數(shù)中搜索 一個(gè)變量的時(shí)候,如果該函數(shù)內(nèi)并沒有聲明這個(gè)變量,那么此次搜索的過程會(huì)隨著代碼執(zhí)行環(huán)境 創(chuàng)建的作用域鏈往外層逐層搜索,一直搜索到全局對(duì)象為止。變量的搜索是從內(nèi)到外而非從外到 內(nèi)的。

下面這段包含了嵌套函數(shù)的代碼,也許能幫助我們加深對(duì)變量搜索過程的理解:

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 變量的生存周期

除了變量的作用域之外,另外一個(gè)跟閉包有關(guān)的概念是變量的生存周期。

對(duì)于全局變量來說,全局變量的生存周期當(dāng)然是永久的,除非我們主動(dòng)銷毀這個(gè)全局變量。

而對(duì)于在函數(shù)內(nèi)用 var 關(guān)鍵字聲明的局部變量來說,當(dāng)退出函數(shù)時(shí),這些局部變量即失去了 它們的價(jià)值,它們都會(huì)隨著函數(shù)調(diào)用的結(jié)束而被銷毀:

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

現(xiàn)在來看看下面這段代碼:

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 封裝變量

閉包可以幫助把一些不需要暴露在全局的變量封裝成“私有變量”。假設(shè)有一個(gè)計(jì)算乘積的
簡(jiǎn)單函數(shù):

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

mult 函數(shù)接受一些 number 類型的參數(shù),并返回這些參數(shù)的乘積。現(xiàn)在我們覺得對(duì)于那些相同 的參數(shù)來說,每次都進(jìn)行計(jì)算是一種浪費(fèi),我們可以加入緩存機(jī)制來提高這個(gè)函數(shù)的性能:

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 這個(gè)變量?jī)H僅在 mult 函數(shù)中被使用,與其讓 cache 變量跟 mult 函數(shù)一起平行 地暴露在全局作用域下,不如把它封閉在 mult 函數(shù)內(nèi)部,這樣可以減少頁面中的全局變量,以 4 避免這個(gè)變量在其他地方被不小心修改而引發(fā)錯(cuò)誤。代碼如下:

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

提煉函數(shù)是代碼重構(gòu)中的一種常見技巧。如果在一個(gè)大函數(shù)中有一些代碼塊能夠獨(dú)立出來, 我們常常把這些代碼塊封裝在獨(dú)立的小函數(shù)里面。獨(dú)立出來的小函數(shù)有助于代碼復(fù)用,如果這些 小函數(shù)有一個(gè)良好的命名,它們本身也起到了注釋的作用。如果這些小函數(shù)不需要在程序的其他 9 地方使用,最好是把它們用閉包封閉起來。代碼如下:

var cache = {};
var mult = (function(){
    var cache = {};
    var calculate = function(){ // 封閉 calculate 函數(shù)
        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 延續(xù)局部變量的壽命

img 對(duì)象經(jīng)常用于進(jìn)行數(shù)據(jù)上報(bào),如下所示:

var report = function( src ){
    var img = new Image();
    img.src = src;
};
report( 'http://xxx.com/getUserInfo' );

但是通過查詢后臺(tái)的記錄我們得知,因?yàn)橐恍┑桶姹緸g覽器的實(shí)現(xiàn)存在 bug,在這些瀏覽器下使用 report 函數(shù)進(jìn)行數(shù)據(jù)上報(bào)會(huì)丟失 30%左右的數(shù)據(jù),也就是說, report 函數(shù)并不是每一次都成功發(fā)起了 HTTP 請(qǐng)求。丟失數(shù)據(jù)的原因是 img 是 report 函數(shù)中的局部變量,當(dāng) report 函數(shù)的調(diào)用結(jié)束后, img 局部變量隨即被銷毀,而此時(shí)或許還沒來得及發(fā)出 HTTP 請(qǐng)求,所以此次請(qǐng)求就會(huì)丟失掉。

現(xiàn)在我們把 img 變量用閉包封閉起來,便能解決請(qǐng)求丟失的問題:

var report = (function(){
    var imgs = [];
    return function( src ){
        var img = new Image();
        imgs.push( img );
        img.src = src;
    }
})();

二、高階函數(shù)

高階函數(shù)是指至少滿足下列條件之一的函數(shù)。

  • 函數(shù)可以作為參數(shù)被傳遞;
  • 函數(shù)可以作為返回值輸出。

JavaScript 語言中的函數(shù)顯然滿足高階函數(shù)的條件,在實(shí)際開發(fā)中,無論是將函數(shù)當(dāng)作參數(shù)
傳遞,還是讓函數(shù)的執(zhí)行結(jié)果返回另外一個(gè)函數(shù),這兩種情形都有很多應(yīng)用場(chǎng)景,下面就列舉一
些高階函數(shù)的應(yīng)用場(chǎng)景。

2.1 函數(shù)作為參數(shù)傳遞

把函數(shù)當(dāng)作參數(shù)傳遞,這代表我們可以抽離出一部分容易變化的業(yè)務(wù)邏輯,把這部分業(yè)務(wù)邏
輯放在函數(shù)參數(shù)中,這樣一來可以分離業(yè)務(wù)代碼中變化與不變的部分。其中一個(gè)重要應(yīng)用場(chǎng)景就
是常見的回調(diào)函數(shù)。

1. 回調(diào)函數(shù)

在 ajax 異步請(qǐng)求的應(yīng)用中,回調(diào)函數(shù)的使用非常頻繁。當(dāng)我們想在 ajax 請(qǐng)求返回之后做一
些事情,但又并不知道請(qǐng)求返回的確切時(shí)間時(shí),最常見的方案就是把 callback 函數(shù)當(dāng)作參數(shù)傳入
發(fā)起 ajax 請(qǐng)求的方法中,待請(qǐng)求完成之后執(zhí)行 callback 函數(shù):

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

回調(diào)函數(shù)的應(yīng)用不僅只在異步請(qǐng)求中,當(dāng)一個(gè)函數(shù)不適合執(zhí)行一些請(qǐng)求時(shí),我們也可以把這些請(qǐng)求封裝成一個(gè)函數(shù),并把它作為參數(shù)傳遞給另外一個(gè)函數(shù),“委托”給另外一個(gè)函數(shù)來執(zhí)行。

2. Array.prototype.sort

Array.prototype.sort 接受一個(gè)函數(shù)當(dāng)作參數(shù),這個(gè)函數(shù)里面封裝了數(shù)組元素的排序規(guī)則。從Array.prototype.sort 的使用可以看到,我們的目的是對(duì)數(shù)組進(jìn)行排序,這是不變的部分;而使用 什 么 規(guī) 則 去 排 序 , 則 是 可 變 的 部 分 。 把 可 變 的 部 分 封 裝 在 函 數(shù) 參 數(shù) 里 , 動(dòng) 態(tài) 傳 入Array.prototype.sort,使 Array.prototype.sort 方法成為了一個(gè)非常靈活的方法,代碼如下:

//從小到大排列
[ 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 函數(shù)作為返回值輸出

相比把函數(shù)當(dāng)作參數(shù)傳遞,函數(shù)當(dāng)作返回值輸出的應(yīng)用場(chǎng)景也許更多,也更能體現(xiàn)函數(shù)式編程的巧妙。讓函數(shù)繼續(xù)返回一個(gè)可執(zhí)行的函數(shù),意味著運(yùn)算過程是可延續(xù)的。

1. 判斷數(shù)據(jù)的類型

我們來看看這個(gè)例子,判斷一個(gè)數(shù)據(jù)是否是數(shù)組,在以往的實(shí)現(xiàn)中,可以基于鴨子類型的概念來判斷,比如判斷這個(gè)數(shù)據(jù)有沒有 length 屬性,有沒有 sort 方法或者 slice 方法等。但更好的方式是用 Object.prototype.toString 來計(jì)算。 Object.prototype.toString.call( obj )返回一個(gè)字 符 串 , 比 如 Object.prototype.toString.call( [1,2,3] ) 總 是 返 回 "[object Array]" , 而Object.prototype.toString.call( “str”)總是返回"[object String]"。所以我們可以編寫一系列的isType 函數(shù)。代碼如下:

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]';
};

我們發(fā)現(xiàn),這些函數(shù)的大部分實(shí)現(xiàn)都是相同的,不同的只是 Object.prototype.toString.call( obj )返回的字符串。為了避免多余的代碼,我們嘗試把這些字符串作為參數(shù)提前值入 isType函數(shù)。代碼如下:

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

我們還可以用循環(huán)語句,來批量注冊(cè)這些 isType 函數(shù):

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

下面是一個(gè)單例模式的例子,在第三部分設(shè)計(jì)模式的學(xué)習(xí)中,我們將進(jìn)行更深入的講解,這
里暫且只了解其代碼實(shí)現(xiàn):

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

這個(gè)高階函數(shù)的例子,既把函數(shù)當(dāng)作參數(shù)傳遞,又讓函數(shù)執(zhí)行后返回了另外一個(gè)函數(shù)。我們可以看看 getSingle 函數(shù)的效果:

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

注:內(nèi)容摘取《Javascript設(shè)計(jì)模式與開發(fā)實(shí)踐》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,682評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,435評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,665評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,838評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,379評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,627評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容

  • 一切皆對(duì)象 js中的一個(gè)常見運(yùn)算符 typeof 以上代碼列出了 typeof 輸出的集中類型標(biāo)識(shí), 其中上面的四...
    無跡落花閱讀 2,010評(píng)論 0 5
  • JavaScript,通常縮寫為 JS,是一種解釋執(zhí)行的編程語言。它是現(xiàn)在最流行的腳本語言之一。 JavaScri...
    神齊閱讀 4,947評(píng)論 1 32
  • 函數(shù)和對(duì)象 1、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語言來說都是核心的概念。通過函數(shù)可以封裝任意多條語句,而且...
    道無虛閱讀 4,614評(píng)論 0 5
  • 昨天晚上房東突然給我打了個(gè)電話,其實(shí)內(nèi)心已經(jīng)有了預(yù)感有不好的事情,它打電話只有壞事。但還是接了電話。事實(shí)也是如此。...
    言天8閱讀 425評(píng)論 0 1
  • 千頭萬緒 無處可逃 你永遠(yuǎn)不可能透徹去地了解一個(gè)人 你不知道他的童年如何 他吃過的飯,走過的路 他看過的景,愛過的...
    JasmineGreen閱讀 72評(píng)論 0 0