高阶函数

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

  • 函数可以作为参数传递
  • 函数可以作为返回值输出

JavaScript语言中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数作为参数传递,还是让函数的执行结果返回另外一个函数,这两种情形都有很多应用场景,下面就列举一些高阶函数的应用场景
1、函数作为参数传递
把函数当做参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变化的部分。其中一个重要应用场景就是常见的回调函数。

  • 回调函数

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

var getUserInfo = function( userId, callback ) {
    $.ajax('http://xxx.com/getUserInfo?'+userId,function(data){
        if(typeof callback === 'function') {
            callback( data );
        }
    };
};
getUserInfo( 13175, 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会执行之前客户传入的回调函数。

  • Array.prototype.sort

Array.ptototype.sort接受一个函数当做参数,这个函数里面封装了数组元素的排序规则。从Array.prototype.sort的使用可以看到,我们的目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里,动态传入Array.prototype.sort,使Array.prototype.sort方法成为了一个非常灵活的方法,代码如下:

//从小到大排序
[1,4,3].sort( function(a,b) {
    return a-b;
});
//输出: [1,3,4]

//从大到小排序
[1,4,3].sort( function( a,b ) {
    return b - a;
});
//输出:[4,3,1]

2、函数作为返回值输出
相比把函数当做参数传递,函数当做返回值输出的场景也许更多,也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。

  • 判断数据的类型

我们来看看这个例子,判断一个数据是否是数组,在以往的实现中,可以基于鸭子类型的概念来判断,比如判断这个数据有没有length属性,有没有sort方法或者slice方法等。但更好的方式是用Object.prototype.toString来计算。Object.prototype.toString.call( obj )返回一个字符串,比如Object.prototype.toString.call( [1 , 2 , 3 ] )总是返回“[ object Array]”,而Object.prototype.toString.call( “str” )总是返回“[ object String ]”。所以我们可以编写一系列的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

- getSingle

下面是一个单例模式的例子,在第三部分设计模式的学习中,我们将进行更深入的理解,这里暂且只了解其代码实现:

var getSingle = function( fn ) {
    var ret;
    return function () {
        return ret || ( ret = fn.apply(this, arguments));
    }
};
这个高阶函数的列子,既把函数当做参数传递,又让函数执行后返回了另外一个函数。我们可以看看getSingle函数的效果:

var getScript = getSingle(function(){
    return document.createElement('script');
});
var script1 = getScript();
var script2 = getScript();
alert( script1 === script2 ); // true

3、高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便的复用日志统计等功能模块。
通常,在JavaScript中实现AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多,本节我们通过扩展Function.prototype来做到这一点。代码如下:

Function.prototype.before = function( beforefn ) {
    var _self = this; //保存原函数的引用
    return function(){ //返回包含了原函数和新函数的“代理”函数
        beforefn.apply( this, arguments ); //执行新函数,修正this
        return _self.apply( this, arguments );//执行原函数
    }
};

Function.prototype.after = function( afterfn ) {
    var _self = this;
    return function(){
        var ret=_selft.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};
var func = function(){
    console.log( 2 );
};
func = func.before(function(){
    console.log(1);
}).alter(function(){
    console.log(3);
});
func(); // 1,2,3

这种使用AOP的方式来给函数添加职责,也是JavaScript语言中一种非常特别的巧妙的装饰者模式实现。这种装饰者模式在实际开发中非常有用。
4、高阶函数的其他应用
- currying
- uncurrying
- 函数节流
- 分时函数
- 惰性加载函数
在web开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免。比如我们需要一个在各个浏览器中能够通用的事件绑定函数addEvent,常见的写法如下:

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条件分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。
第二种方案是这样,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就立刻执行一次判断,以便让addEvent返回一个包裹了正确逻辑的函数。代码如下:

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函数,这样看来,前一次的浏览器嗅探就是完成多余的操作,而且这也会稍稍延长页面ready的时间。
第三种方案即是我们将要讨论的惰性载入函数方案。此时addEvent依然被声明为一个普通函数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们期望的addEvent函数,在下一次进入addEvent函数的时候,addEvent函数里不再存在条件分支语句:

<body>
    <div id="div1">点我绑定事件</div>
    <script>
        var addEvent=function(elem,type,handler){
            if(window.addEventListener){
                addEvent = function(elem,type,handler) {
                    elem.addEventListener(type,handler,false);
                }
            } else if (window.attachEvent){
                addEvent = function(elem,type,handler) {
                    elem.attachEvent('on'+type,handler);
                }
            }
            addEvent( elem, type, handler );
        };
        var div = document.getElementById( 'div1' );
        addEvent( div, 'click', function({alert(2)});
    </script>
</body>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值