高阶函数是指满足下列条件之一的函数:

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

函数作为参数传递

把函数当做参数进行传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来就可以分离业务代码中变化和不变的部分。

  • 回调函数

    ajax 异步请求的应用中,回调函数的使用非常频繁。当我们想在 ajax 请求返回之后做些事情,但又并不知道请求返回的确切时间时,最常见的方案是把 callback 函数当做参数传入发起的 ajax 请求的方法中,待请求完成之后执行callback 函数:

    1
    2
    3
    4
    5
    6
    7
    var getUserInfo = function( userId, callback ) {
    $.ajax( 'http://bubuzou.com/getUserInfo?' + userId, function( data ) {
    if ( typeof callback === 'function') {
    callback( data );
    }
    });
    };

    再来看一个例子,假设有这样一个需求,需要创建 100div元素,同时把他们隐藏起来,那么可以看到下面这种实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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 未免有点个性化,成为了一个难以复用的函数,并不是每个人创建了节点之后就希望它们立刻被隐藏。于是我们把这段代码抽离出来,用回调函数的形式传入 appendDiv 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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';
    });
  • Array.prototype.sort

    Array.prototype.sort 接受一个函数作为参数,这个函数里面封装了数组元素的排序顺序。我们的目的是对数组进行排序,这是不变的部分;但用什么规则去排序这是可变的部分。

    1
    2
    3
    4
    // 从小到大排序
    [3, 9, 8, 5].sort(function(a, b) {
    return a - b;
    });

函数作为返回值输出

  • 判断数据的类型

    判断一个数据是否是数组,在以往的实现当中,可以基于鸭子类型的概念来判断,比如这个数据有没有 length 属性,有没有sort 方法等。但更好的方法是用 Object.prototype.toString 来计算。根据Object.prototype.toString.call( [1, 2, 3] ) 总是返回 ’[object Array]’Object.prototype.toStrng.call( ‘str’ ) 也总是返回 ’[object Array]’ 得出,
    它总是会返回一个类似结构的字符串。于是用循环语句来批量注册类型判断的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
        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( '' ); // true
    ````

    - getSingle

    下面是一个单例模式的例子:

    ``` js
    var getSingle = function( fn ) {
    var ret;
    return function() {
    return ret || ( ret = fn.apply( this, arguments ));
    };
    };
    // 来看看这个单例的效果
    var getScript = getSingle(function() {
    return document.createElement( 'script' );
    });
    var script1 = getScript();
    var script2 = getScript();
    console.log( script1 === script2 ); // true

高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来后,再通过“动态植入”的方式掺入业务逻辑模块中。这样做的好处是首先保证了业务逻辑模块的纯洁和高内聚性,其实是可以很方便的复用这些日志统计等功能模块。在 JavaScript 中,我们可以通过Function.prototype 来实现 AOP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Function.prototype.before = function( beforeFn) {
var _self = this;
return function() {
beforeFn.apply( this, arguments );
return _self.apply( this, arguments );
}
};
Function.prototype.after = function( afterFn ) {
var _self = this;
return function() {
var ret = _self.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 2 3

高阶函数的其他应用

  • currying

    currying 的概念最早由俄国数学家 Moses Schoofinkel 发明,而后由著名的数理逻辑学家 Haskell Curry 将其丰富和发展,currying 由此得名。

    currying 又称部分求值。这里我们讨论的是函数柯里化( function currying )。一个柯里化函数首先会接受一些参数,接受了这些参数会后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正求值的时候,之前传入的所有参数都会被一次性用于求值。来看一个例子:
    假设我们要编写一个计算每月开销的函数。在每天结束之前,我们都要记录几天花掉了多少钱。

    1
    2
    3
    4
    5
    6
    7
    8
    var monthCost = 0;
    var cost = function( money ) {
    monthCost += money;
    };
    cost( 100 ); 第一天
    cost( 200 ); 第二天
    cost( 300 ); 第三天
    console.log( monthCost ); // 600

    通过这段代码,我们可以看到每天都花了多少钱,但是如果我们只想知道每个月的消费如何的话,那就没必要计算每天的花费了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var cost = (function() {
    var args = [];
    return function() {
    if ( arguments.length === 0 ) {
    var money = 0;
    for ( var i = 0, l = args.length; i < l; i++ ) {
    money += args[i];
    }
    return money;
    } else {
    [].push.apply( args, arguments );
    }
    }
    })();
    cost( 100 );
    cost( 200 );
    cost( 300 );
    console.log( cost() ); // 600

    接下来编写一个通用的柯里化函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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 );
    cost( 100 );
    cost( 200 );
    cost( 300 );
    console.log( cost() ); // 600
  • uncurrying

    uncurrying 是反柯里化,大概意思是扩大函数的应用范围,将本来只有特定对象才能使用的方法,扩展到更多的对象。
    比如我们常常让类数组对象去借用 Array.prototype 的方法:

    1
    2
    3
    4
    (function() {
    Array.prototype.push.call( arguments, 4 );
    console.log( arguments ); // [1, 2, 3, 4]
    })(1, 2, 3);

    uncurrying第一种实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    Function.prototype.uncurrying = function() {
    var self = this;
    return function() {
    var obj = Array.prototype.shift.call( arguments );
    return self.apply( obj, arguments );
    };
    };
    for ( var i = 0, fn, ary = ['push', 'shift', 'forEach']; fn = ary[i++]; ) {
    Array[ fn ] = Array.prototype[ fn ].uncurrying();
    }
    var obj = {
    "length": 3,
    "0": 1,
    "1": 2,
    "2": 3
    };
    Array.push( obj, 4);
    console.log( obj.length ); // 4
    var first = Array.shift( obj );
    console.log( first ); // 1
    console.log( obj ); // {0:2, 1:3, 2:4, length: 3}
    Array.forEach( obj, function( i, n ) {
    console.log( n ); // 分别输出:0, 1, 2
    });

    uncurrying的第二种实现:

    1
    2
    3
    4
    5
    6
    Function.prototype.uncurrying = function() {
    var self = this;
    return function() {
    return Function.prototype.call.apply( self, arguments );
    }
    };
  • 函数节流

    JavaScript 中,大部分的函数都是由用户主动调动触发的。但是也存在少数情况,这些情况下函数的触发并不是又用户直接控制的。这个时候函数就有可能被频繁地调用,而造成大的性能问题。以下几个场景函数将被频繁调用:给 window 绑定了onresize 事件的时候,如果存在 DOM 相关的操作,那这个时候是非常耗性能的,严重的时候浏览器可能会卡顿;mousemove 事件,如果给某个元素绑定了拖拽事件,那么该函数也会被频繁的触发;在比如上传一个文件的时候,可能需要频繁的通知进度信息等。

    函数节流就是为了避免函数被频繁地调用而存在的一种解决方案,从而优化性能。通常是用 setTimeout 来实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    var throttle = function( fn, interval ) {
    var _self = fn,
    timer,
    firstTime = true;
    return function() {
    var args = arguments,
    _me = this;
    if ( firstTime ) {
    _self.apply( _me, args );
    return firstTime = false;
    }
    if ( timer ) {
    return false;
    }
    timer = setTimeout(function() {
    clearTimeout( timer );
    timer = null;
    _self.apply( _me, args );
    }, interval || 500 );
    }
    };
    window.onresize = throttle(function() {
    console.log( 1 );
    }, 500);
  • 分时函数

    上面我们介绍了一种解决函数被频繁调用的方法。但是有时候,用户确实有这种需求,比如需要在短时间内把 1000qq 好友渲染到列表上,这个时候就可能会很卡。但是如果把 1000ms 创建 1000 个节点,改成每 200ms 创建 8 个节点。这个时候就避免这种问题。
    分时函数接受 3 个参数:第一个是创建节点的时候需要用到的数据,第二个是封装了创建节点的函数,第三个是每一批创建的节点数量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var timeChunk = function( ary, fn, count ) {
    var obj,
    t,
    start;
    start = function() {
    for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ) {
    var obj = ary.shift();
    fn( obj );
    }
    };
    return function() {
    t = setInterval(function() {
    if ( ary.length === 0 ) { // 如果全部节点都已经创建好
    clearInterval( t );
    }
    start();
    }, 200);
    };
    };

    分时函数有了,现在我们来测试一下。假设有 1000 个好友,利用 timeChunk 函数,每批往页面上渲染 8 个:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var ary = [];
    for ( var i = 0; i <= 1000; i++ ) {
    ary.push( i );
    }
    var renderFriendList = timeChunk( ary, function( n ) {
    var div = document.createElement( 'div' );
    div.innerHTML = n;
    document.body.appendChild( div );
    }, 8);
    renderFriendList();
  • 惰性加载函数

    web 开发的过程中,因为浏览器之间的实现差异,一些嗅探工作总是避免不了的。比如我们需要一个能在各个浏览器都能通用的事件绑定函数 addEvent,常见的写法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    var addEvent = function( elem, type, handler ) {
    if ( window.addEventListener ) {
    return elem.addEventListener( type, handler, false );
    }
    if ( window.attachEvent ) {
    return elem.attachEvent( 'on' + type, handler );
    }
    };

    这种写法的缺点是每次调用函数都必须执行里面的if判断,虽然开销不大,但是有办法能避免这种操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var addEvent = (function() {
    if ( window.addEventListener ) {
    return function( elem, type, handler ) {
    elem.addEventListener( type, handler, false );
    }
    }
    if ( window.attachEvent ) {
    return function( elem, type, handler ) {
    elem.attachEvent( 'on' + type, handler );
    }
    }
    })();

    把嗅探的操作提前到代码加载之前,在代码加载的时候就即可进行一次判断,以便让 addEvent返回一个正确的事件绑定函数。
    但是这种写法还是存在缺点的,如果我们从头到尾都不需要进行事件绑定,那么前面那次的嗅探动作就显得多余了。
    第三种方案是惰性载入函数方案,第一次进入 addEvent 函数的时候会重写事件绑定函数,在下次进去的时候就会直接执行事件绑定了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var addEvent = function( elem, type, handler ) {
    if ( window.addEventListener ) {
    addEvent = function( elem, type, handler ) {
    elem.addEventListener( type, handler, false );
    }
    }
    if ( window.attachEvent ) {
    addEvent = function( elem, type, handler ) {
    elem.attachEvent( 'on' + type, handler );
    }
    }
    addEvent( elem, type, handler );
    };