JS 高阶函数

高阶函数


高阶函数的基础使用,主要包括参数传递和返回值输出两种形式。其中,高阶函数的一个重要应用是函数柯里化(currying)。

[1] 定义

高阶函数(higher-order function)指操作函数的函数,一般地,有以下两种情况

1、函数可以作为参数被传递

2、函数可以作为返回值输出

JavaScript中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数当作参数传递,还是让函数的执行结果返回另外一个函数,这两种情形都有很多应用场景。

[2] 参数传递

把函数当作参数传递,代表可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个常见的应用场景就是回调函数

【 回调函数 】

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

[3] 返回值输出

相比把函数当作参数传递,函数当作返回值输出的应用场景也有很多。让函数继续返回一个可执行的函数,意味着运算过程是可延续的

下面是使用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

[4] 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

[5] 其他应用

【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的概念最早由俄国数学家Moses Schönfinkel发明,而后由著名的数理逻辑学家Haskell Curry将其丰富和发展,currying由此得名。

 

定义


currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值

从字面上理解currying并不太容易,下面通过编写一个计算每月开销的函数来解释函数柯里化currying


 

每月开销函数


在每天结束之前,都要记录今天花掉了多少钱。代码如下:

var monthlyCost = 0;
var cost = function( money ){ 
  monthlyCost += money;
};
cost( 100 ); // 第 1 天开销 
cost( 200 ); // 第 2 天开销 
cost( 300 );   // 第 3 天开销
//...
cost( 700 );   // 第 30 天开销
alert ( monthlyCost );     // 输出1个月的总开销

每天结束后都会记录并计算到今天为止花掉的钱。但其实并不太关心每天花掉了多少钱,而只想知道到月底的时候会花掉多少钱。也就是说,实际上只需要在月底计算一次

如果在每个月的前29天,都只是保存好当天的开销,直到最后一天才进行求值计算,这样就达到了我们的要求,代码如下

  var cost = (function () {
    var args = [];
    return function () {
      //如果没有参数,则计算args数组中的和
      if (arguments.length === 0) {
        var money = 0;
        for (var i = 0, l = args.length; i < l; i++) {
          money += args[i];
        }
        return money;
        //如果有参数,则只能是将数据传到args数组中
      } else {
        [].push.apply(args, arguments);
      }
    }
  })();
  cost(100); // 未真正求值 
  cost(200); // 未真正求值 
  cost(300); // 未真正求值
  console.log(cost()); // 求值并输出:600

 

通用函数


下面来编写一个通用的柯里化函数currying,currying接受一个参数,即将要被currying的函数。如果和上面的例子结合,则这个函数的作用是遍历本月每天的开销并求出它们的总和

  var currying = function (fn) {
    var args = [];
    return function () {
      if (arguments.length === 0) {
        return fn.apply(this, args);
      } else {
        [].push.apply(args, arguments);
        return arguments.callee;
      }
    }
  };
  var cost = (function () {
    var money = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        money += arguments[i];
      }
      return money;
    }
  })();
  var cost = currying(cost); // 转化成 currying 函数
  cost(100); // 未真正求值 
  cost(200); // 未真正求值 
  cost(300);   // 未真正求值
  alert(cost());  // 求值并输出:600

至此,完成了一个currying函数的编写。当调用cost()时,如果明确地带上了一些参数,表示此时并不进行真正的求值计算,而是把这些参数保存起来,此时让cost函数返回另外一个函数。只有以不带参数的形式执行cost()时,才利用前面保存的所有参数,真正开始进行求值计算


 

可传参函数


实际上,柯里化函数不仅可以接收要柯里化的函数作为参数,也可以接收一些必要参数,下面是函数柯里化(currying)的改进代码

  var currying = function (fn) {
    var args = [];
    //储存传到curring函数中的除了fn之外的其他参数,并储存到args函数中
    args = args.concat([].slice.call(arguments,1));
    return function () {
      if (arguments.length === 0) {
        return fn.apply(this, args);
      } else {
        //将fn中的参数展开,然后再储存到args数组中
        [].push.apply(args, arguments);
      }
    }
  };
  var cost = (function () {
    var money = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        money += arguments[i];
      }
      return money;
    }
  })();
  var cost = currying(cost,100,200); // 转化成 currying 函数
  cost(100,200); // 未真正求值 
  cost(300);   // 未真正求值
  console.log((cost()));  // 求值并输出:900

 

求值柯里化


如果函数柯里化(curring)之后,传参的同时伴随着求值的过程,则代码简化如下

  var currying = function (fn) {
    //获取除了fn之外的其他参数
    var args = [].slice.call(arguments, 1);
    return function () {
      //获取fn里的所有参数
      var innerArgs = [].slice.call(arguments);
      //最终的参数列表为args和innerArgs的结合
      var finalArgs = args.concat(innerArgs);
      //将finalArgs里的参数展开,传到fn中执行
      return fn.apply(null, finalArgs);
    };
  };
  var cost = (function () {
    var money = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        money += arguments[i];
      }
      return money;
    }
  })();
  var cost = currying(cost,100,200); // 转化成 currying 函数
  cost(300);//100+200+300=600
  cost(100,100);//(100+200+300)+(100+200+100+100)=1100

 

反柯里化


Array.prototype上的方法原本只能用来操作array对象。但用call和apply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性

有没有办法把泛化this的过程提取出来呢?反柯里化(uncurrying)就是用来解决这个问题的。反柯里化主要用于扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。

uncurrying的话题来自JavaScript之父Brendan Eich在2011年发表的一篇文章。以下代码是 uncurrying 的实现方式之一:

Function.prototype.uncurrying = function () { 
  var _this = this;
  return function() {
    var obj = Array.prototype.shift.call( arguments );
    return _this.apply( obj, arguments );
  };
};

另一种实现方法如下

Function.prototype.currying = function() {
    var _this = this;
    return function() {
        return Function.prototype.call.apply(_this, arguments);
    }
}

最终是都把this.method转化成method(this,arg1,arg2....)以实现方法借用和this的泛化

下面是一个让普通对象具备push方法的例子

 var push = Array.prototype.push.uncurrying(),
    obj = {};
  push(obj, 'first', 'two');
  console.log(obj);
/*obj {
    0 : "first",
    1 : "two"
}*/

通过uncurrying的方式,Array.prototype.push.call变成了一个通用的push函数。这样一来,push函数的作用就跟Array.prototype.push一样了,同样不仅仅局限于只能操作array对象。而对于使用者而言,调用push函数的方式也显得更加简洁和意图明了

最后,再看一个例子

var toUpperCase = String.prototype.toUpperCase.uncurrying();
console.log(toUpperCase('avd')); // AVD
function AryUpper(ary) {
    return ary.map(toUpperCase);
}
console.log(AryUpper(['a', 'b', 'c'])); // ["A", "B", "C"]