原文地址: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/
