[1]定義 [2]參數傳遞 [3]返回值輸出 [4]AOP [5]其他應用 ...
前面的話
前面的函數系列中介紹了函數的基礎用法。從本文開始,將介紹javascript函數進階系列,本文將詳細介紹高階函數
定義
高階函數(higher-order function)指操作函數的函數,一般地,有以下兩種情況
1、函數可以作為參數被傳遞
2、函數可以作為返回值輸出
javascript中的函數顯然滿足高階函數的條件,在實際開發中,無論是將函數當作參數傳遞,還是讓函數的執行結果返回另外一個函數,這兩種情形都有很多應用場景。下麵將對這兩種情況進行詳細介紹
參數傳遞
把函數當作參數傳遞,代表可以抽離出一部分容易變化的業務邏輯,把這部分業務邏輯放在函數參數中,這樣一來可以分離業務代碼中變化與不變的部分。其中一個常見的應用場景就是回調函數
【回調函數】
在ajax非同步請求的應用中,回調函數的使用非常頻繁。想在ajax請求返回之後做一些事情,但又並不知道請求返回的確切時間時,最常見的方案就是把callback函數當作參數傳入發起ajax請求的方法中,待請求完成之後執行callback函數
var getUserInfo = function( userId, callback ){ $.ajax( 'http://xx.com/getUserInfo?' + userId, function( data ){ if ( typeof callback === 'function' ){ callback( data ); } }); } getUserInfo( 123, function( data ){ alert ( data.userName ); });
回調函數的應用不僅只在非同步請求中,當一個函數不適合執行一些請求時,也可以把這些請求封裝成一個函數,並把它作為參數傳遞給另外一個函數,“委托”給另外一個函數來執行
比如,想在頁面中創建100個div節點,然後把這些div節點都設置為隱藏。下麵是一種編寫代碼的方式:
var appendDiv = function(){ for ( var i = 0; i < 100; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = i; document.body.appendChild( div ); div.style.display = 'none'; } }; appendDiv();
把div.style.display = 'none'的邏輯硬編碼在appendDiv里顯然是不合理的,appendDiv未免有點個性化,成為了一個難以復用的函數,並不是每個人創建了節點之後就希望它們立刻被隱藏
於是把div.style.display = 'none'這行代碼抽出來,用回調函數的形式傳入appendDiv方法
var appendDiv = function( callback ){ for ( var i = 0; i < 100; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = i; document.body.appendChild( div ); if ( typeof callback === 'function' ){ callback( div ); } } }; appendDiv(function( node ){ node.style.display = 'none'; });
可以看到,隱藏節點的請求實際上是由客戶發起的,但是客戶並不知道節點什麼時候會創建好,於是把隱藏節點的邏輯放在回調函數中,“委托”給appendDiv方法。appendDiv方法當然知道節點什麼時候創建好,所以在節點創建好的時候,appendDiv會執行之前客戶傳入的回調函數
【數組排序】
函數作為參數傳遞的另一個常見場景是數組排序函數sort()。Array.prototype.sort接受一個函數當作參數,這個函數裡面封裝了數組元素的排序方法。目的是對數組進行排序,這是不變的部分;而使用什麼規則去排序,則是可變的部分。把可變的部分封裝在函數參數里,動態傳入Array.prototype.sort,使Array.prototype.sort方法成為了一個非常靈活的方法
// 從小到大排列,輸出: [ 1, 3, 4 ] [ 1, 4, 3 ].sort( function( a, b ){ return a - b; }); // 從大到小排列,輸出: [ 4, 3, 1 ] [ 1, 4, 3 ].sort( function( a, b ){ return b - a; });
返回值輸出
相比把函數當作參數傳遞,函數當作返回值輸出的應用場景也有很多。讓函數繼續返回一個可執行的函數,意味著運算過程是可延續的
下麵是使用Object,prototype.toString方法判斷數據類型的一系列的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
AOP
AOP(面向切麵編程)的主要作用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能通常包括日誌統計、安全控制、異常處理等。把這些功能抽離出來之後,再通過“動態織入”的方式摻入業務邏輯模塊中。這樣做的好處首先是可以保持業務邏輯模塊的純凈和高內聚性,其次是可以很方便地復用日誌統計等功能模塊
通常,在javascript中實現AOP,都是指把一個函數“動態織入”到另外一個函數之中。下麵通過擴展Function.prototype來實現
Function.prototype.before = function (beforefn) { var _this = this; // 保存原函數的引用 return function () { // 返回包含了原函數和新函數的"代理"函數 beforefn.apply(this, arguments); // 先執行新函數,修正this return _this.apply(this, arguments); // 再執行原函數 } }; Function.prototype.after = function (afterfn) { var _this = this; return function () { var ret = _this.apply(this, arguments); //先執行原函數 afterfn.apply(this, arguments); //再執行新函數 return ret; } }; var func = function () { console.log(2); }; func = func.before(function () { console.log(1); }).after(function () { console.log(3); }); func();
把負責列印數字1和列印數字3的兩個函數通過AOP的方式動態植入func函數。通過執行上面的代碼,控制台順利地返回了執行結果1、2、3
//1 //2 //3
其他應用
【not】
下麵的not函數用於返回參數的返回值的邏輯非
function not(f) { return function () { return !(f.apply(this, arguments)); }; } //偶數時,返回true;奇數時,返回false var even = function (x) { return x % 2 === 0; } //偶數時,返回false;奇數時,返回true var odd = not(even); [1, 1, 3, 5, 5].every(odd);//true
【mapper】
下麵的mapper()函數,返回的新函數將一個數組映射到另一個使用這個函數的數組上
//所返回的函數的參數應當是一個實參數組,並對每個數組元素執行函數f(),並返回所有計算結果組成的數組 function mapper(f){ return function(a){ return Array.prototype.map.call(a,f); } } var increment = function(x){ return x+1; } var incrementer = mapper(increment); increment([1,2,3]);//[2,3,4]
【squareofsum】
下麵的函數接收兩個函數f()和g(),並返回一個新函數用以計算f(g())
//返回一個新的可以計算f(g(...))的函數 //返回的函數h()將它所有的實參傳入g(),然後將g()的返回值傳入f() //調用f()和g()時的this值和調用h()時的this值是同一個this function compose(f,g){ return function(){ //需要給f()傳入一個參數,所以使用f()的call()方法 //需要給g()傳入很多參數,所以使用g()的apply()方法 return f.call(this,g.apply(this,arguments)); }; } var square = function(x){ return x*x; } var sum = function(x,y){ return x + y; } var squareofsum = compose(square,sum); squareofsum(2,3);//25
上面代碼中,首先執行compose(square,sum)。square傳給f,sum傳給g。然後執行f(g())。g作為f函數的參數,首先執行。即先執行sum(2,3),結果為5。再執行square(5),最終結果為25
最後
本文介紹了高階函數的基礎使用,主要包括參數傳遞和返回值輸出兩種形式。其中,高階函數的一個重要應用是函數柯里化(currying),將在下篇博文中詳細介紹