原文地址:http://www.moye.me/2014/12/29/closure_higher-order-function/
引子
最近發現一個問題:一部分寫JS的人,其實對於函數式編程的概念並不是太了解。如下的代碼片斷,常常讓他們覺得不可思議:
OAuth2Server.prototype.authCodeGrant = function (check) { var self = this; return function (req, res, next) { new AuthCodeGrant(self, req, res, next, check); }; };
上述片斷來自開源項目node-oauth2-server,這個authCodeGrant原型函數涉及到JS編程中經常用到的兩個概念:閉包 和 高階函數(check變量在這個函數中被閉包,authCodeGrant能返回函數,因此是一個高階函數。
閉包
閉包就是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。
如何來理解這個自由變量呢?
自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量
什么樣的變量是自由變量呢?如下片斷中的freeVar對inner()
來說就是個自由變量:
function wrapper() { var freeVar = 42; function inner() { return 2 * freeVar; } return inner; }
自由變量在閉包生成之前,並不是函數的一部分。在函數被調用時,閉包才會形成,函數將這個自由變量納入自己的作用域,也就是說,自由變量從此與定義它的容器無關,以函數被調用那一刻為時間點,成為函數Context中的成員。
來看一個困惑前端的示例,循環添加事件:
<button>第1條記錄</button> <button>第2條記錄</button> <button>第3條記錄</button> <button>第4條記錄</button> <button>第5條記錄</button> <button>第6條記錄</button> <script type="text/javascript"> var buttonst_obj = document.getElementsByTagName("button"); for (var i = 0, len = buttonst_obj.length; i < len; i++) { buttonst_obj[i].onclick = function() { alert(i); }; } </script>
上述片斷的結果是:每個Button彈出的都是6。因為沒有形成有效的閉包,因為閉包是有延遲求值特性的,所以在函數得到執行時,i === 6。
如果我們將它改成這樣,i 做為外層函數的參數而被內層函數閉包,結果也是我們想要的:
var buttonst_obj = document.getElementsByTagName("button"); for (var i = 0, len = buttonst_obj.length; i < len; i++) { buttonst_obj[i].onclick = clickEvent(i); } function clickEvent(i){ return function () { console.log(i); } }
Why? 因為這個clickEvent(i)
高階函數,它將 i 作為自由變量(注意:i 並不是內函數的參數,也不是內函數的一部分)傳遞,在 click 時閉包已經形成並被傳遞。
閉包的作用域
雖然自由變量從閉包時起 “將和這個函數一同存在,即使已經離開了創造它的環境也不例外”,但我們必須搞清楚,閉包產生時的作用域,看個例子:
var scope = 'global'; function echo(){ console.log(scope); } function wrapper(){ var scope = 'inner'; echo(); } echo(); // 輸出global wrapper(); // 輸出global
為什么在wrapper內部的echo()調用,會輸出全局scope?因為:echo定義的位置,只能閉包到全局的scope,它的外層作用域就是全局空間,即便是延遲求值也如此。
把這段代碼稍加改造,就能看得更清楚:
var scope = 'global'; function echo(){ console.log(scope); } function wrapper(){ var scope = 'inner'; function echo(){ console.log(scope); } echo(); } echo(); //輸出global wrapper(); //輸出inner
閉包的自由變量來自何處,和它的外層作用域(被定義的位置)也是有關系的。
高階函數
上述循環事件片斷中的 clickEvent(i)
即為一個高階函數。
高階函數滿足:要么接受一個或多個函數作為輸入;要么輸出一個函數
為什么會用到高階函數?粗糙的說,就是為了閉包。
接受函數作為輸入的高階函數
這種高階函數可作為一種模式的構造器,比如:我有快速排序/堆排序/希爾排序 等若干個排序函數,那么我只需要提供一個高階函數,就能生成基於這若干種排序函數的排序器:
//排序器 var sortingGenerator = function(sortFunc){ return function(args){ var arguments = [].slice.call(args); return sortFunc(arguments); } }; //引入排序算法 var heapSort = require('heapSort'); var heapSorter = sortingGenerator(heapSort); //使用算法 heapSorter(4, 22, 44, 66, 77);
當然,其實這個高階函數也輸出了函數
輸出函數的高階函數
和上例一樣,高階函數輸出一個函數也很好理解:先閉包自由變量,根據它在將來調用時產生不一樣的輸出。
比如,我需要一個函數,既可以算平方,也可以算立方,最好什么方都能算,這時我就需要一個如下片斷的高階函數:
//計算m的N次方 var powerOfN = function(n){ return function(m){ var res = 1; for(var i = 0; i < n; ++i){ res *= m; } return res; } ; }; //按需生成 var powerOf2 = powerOfN(2); var powerOf3 = powerOfN(3); //調用傳參 console.log(powerOf2(3)); console.log(powerOf3(2));
小結
通過閉包和高階函數的組合運用,我們可以提煉出這樣一種編程模式:通過分離>=2次的參數傳遞,以最少的代碼實現動態的算法生成器。
更多文章請移步我的blog新地址: http://www.moye.me/