阅读 155

设计模式基础 之 4 高阶函数

1 什么是高阶函数

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

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

2 函数作为参数传递

函数作为参数传递,代表我们可以抽离出一部分变化的业务逻辑,将其放入到函数参数中。这样就可以分离业务代码中变化与不变的部分,其中要给重要的场景就是常见的回调函数。

var getUserInfo = function(userId, callback) {
    $.ajax('http://xxx.w.com?', function(data){
        if (typeof callback === 'function') {
            callback(data);
        }
    });
}
getUserInfo(12, function(data) {
    console.log('user message is :' + data);
})
复制代码

3 函数作为返回值输出

相比把函数当作参数传递,函数当作返回值输出的应用场景也许更多,也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。 我们需要对数据类型进行判断,例如判断是否是数组,是否是字符串等。

var isArray = function(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
}
var isString = function(obj) {
    return Object.prototype.toString.call(obj) === '[object String]';
}
var isNumber= function(obj) {
    return Object.prototype.toString.call(obj) === '[object Number]';
}
复制代码

但是实际上上面的内容都重复了,有很多冗余的代码,下面我们将函数座位返回值输出的方式:

var isType = function(type) {
    return function() {
        return Object.prototype.toString.call(obj) === `[object ${type}]`
    }
};
var isArray = isType(Array);
var isString = isType(String);
var isNumber = isType(Number);
复制代码

我们也可以通过循环的方式来进行注册:

var Type = {};
var types = ['String', 'Array', 'Number'];
// map方式已经是闭包的方式,将数组元素作为参数传递
types.map(function(type) {
    Type[`is${type}`] = function(obj) {
        return Object.prototype.toString.call(obj) === `[object ${type}]`
    }
})
// map循环等价
for(i = 0; i < types.length; i++) {
    var type = types[i];
    (function(type) {
        Type[`is${type}`] = function(obj) {
        console.log(type);
        return Object.prototype.toString.call(obj) === `[object ${type}]`
        }
    })(type);
}

Type.isString('hello');
复制代码

4 高阶函数实现AOP

AOP(面向切面编程)的主要作用是将一些跟核心业务逻辑无关的功能抽离出来,例如日志统计,安全控制,异常处理等。抽离出来后,再通过动态织入的方式掺入到业务逻辑模块中。这样可以保证业务逻辑模块的纯净和高内聚性,其次可以很方便地统计和服用日志统计功能模块。javascript能够很方便的实现AOP,这里通过Function.prototype来实现。这种方式其实是设计模式中的装饰模式的实现。

Function.prototype.before = function(beforeFunc) {
    var _self = this; // 保留原函数的引用,本例子中是testAop

    // 返回包含原函数和新函数的'代理'函数
    return function() {
        console.log('this是window', this);
        beforeFunc.apply(this, arguments); // 执行代理函数,修正this
        var ret = _self.apply(this, arguments); // 执行原函数(window)
        return ret;
    }
}

Function.prototype.after = function(afterFunc) {
    var _self = this;
    return function() {
        var ret = _self.apply(this, arguments)
        afterFunc.apply(this, arguments);
        return ret;
    }
}

function beforeFunc() {
    console.log('beforeFunc', arguments);
}

function afterFunc() {
    console.log('afterFunc', arguments);
}

function testAop(args1, args2) {
    console.log('arg1s:' + args1 + ', args2: ' + args2);
}

// testAop.before(beforeFunc)().after(afterFunc)();
var func = testAop.before(beforeFunc).after(afterFunc);
conso
func(10, 20);
复制代码

5 高阶函数其他使用

5.1 currying(柯里化)

函数柯里化(function currying): 以人名为名称,currying又称为部分求值。一个currying的函数会接收一些参数,但是接收这些参数后,不会立即求值,而是继续返回另外一个函数。例如上面的AOP例子中,刚传入的参数在函数中形成的闭包保存了起来,等到函数真正需要求值的时候,再讲之前传入的所有参数被一次性用于求值。 下面使用currying方式完成一个任务:一个月在29日之前都存储每一天花费了多少钱,在最后一天才进行结算总共花费额度。

var curryingFunc = function(fun) {
    console.log(fun);
    var moneys = [];
    return function() {
        // 如果是29日以内,不会传入计算金额,因此计算花费总额,否则还是返回当前函数,存储当日花费金额
        if (arguments.length === 0) {
            // 将money作为参数传入
            return fun.apply(this, moneys);
        } else {
            [].push.apply(moneys, arguments);
            return arguments.callee;
        }
    };
};

var cost1 = (function() {
    var money = 0;
    return function() {
        return [].reduce.call(arguments, function(a, b) {
            return a + b;
        }, 0);
    };
})();

var cost = curryingFunc(cost1);
cost(10);
cost(20);
cost();
复制代码

5.2 uncurrying(反柯里化)

  • 柯里化是为了缩小适用范围,创建一个针对性更强的函数;
  • 反柯里化则是扩大适用范围,创建一个应用范围更广的函数。
    当我们调用对象的某个方法时,不需要关系该对象的设计是否拥有这个方法。这就是动态语言的特点,也就是Duck Typing(鸭子类型)。我们可以通过call, apply借用不属于对象身上的方法。而通过call, apply方式的话,需要传入this, 那么我们应该可以将this提取出来,而这就是uncurrying方式:
Function.prototype.uncurrying = function() {
    var _self = this;
    return function() {
		// 需要调用的方法
        var obj = Array.prototype.shift.call(arguments);
        // 剩下的参数([obj, 10])作为参数传入call方法中,上下文对象是obj, 参数是10。
        // 数据存储: 10是闭包参数,保持在内存中让obj对象可以访问。而this.name是使用的obj对象自身的属性。
        _self.apply(obj, arguments)
    };
};
复制代码
  • 通过上面的方法,对Array.push方法进行uncurrying。
var push = Array.prototype.push.uncurrying();
var a = {
    0: 1,
    1: 2,
    length: 2
};
push(a, 3); // {0: 1, 1: 2, 2: 3, length: 3}
复制代码
  • 通过上面的方法,对call方法进行uncurrying:
var call =  Function.prototype.call.uncurrying();
function test(age) {
    console.log(`name : ${this.name}, age: ${age}`);
}
var obj = {
    name: 'yezi'
}
call(test, obj , 10); // name : yezi, age: 10
复制代码

对于call方法的调用,其实在uncurrying内部就是这样的:

Function.prototype.uncurrying = function() {
    var _self = this;
    return function() {
       return Function.prototype.call.apply(_self, arguments)
    }
}
复制代码

5.3 函数节流

javascript中的函数一般都是用户自己触发的,但是有一部分不是由用户自己控制的。在这些场景下,函数被频繁地调用,会引起大的性能问题。之前我做项目中,在完成地图的模块:上面显示很多攻击数据,而数据是从后端获取数据口生成的,数据量比较大。当调整浏览器窗口的时候,会引起界面重绘,会出现卡顿现象。

  • window.onresize(): 触发的是该方法,当浏览器窗口被拖动而改变大小,我们在该方法中进行了DOM节点的相关操作,而DOM节点相关操作是非常损耗性能的。就会出现浏览器卡顿而吃不消。
  • window.onresize(): 如果给一个div绑定了拖拽事件,当div被拖拽时,也会频繁触发该事件
  • 上传进度:有些上传插件在浏览器中进行真正上传之前,会对文件进行扫描并随时通知javascript函数,以便在页面中显示当前的扫描进度。但是该插件通知的频率非常高。

函数节流原理: 上面的三个场景它们的共同问题是:函数触发的频率太高。例如拖拽浏览器窗口大小,窗口打印是1秒钟进行了10次请求,而我们只需要2,3次。我们可以按照时间段来忽略掉一些事件请求。例如确保500ms只打印一次。很显然,我们可以通过setTimeout来完成。 函数节流实现: 实现函数节流的代码有许多种,下面实现的原理是:将即将被执行的函数用setTimeout延时,如果该次延迟执行的还没有完成,那么忽略接下来调用该函数的请求。函数接收2个参数:第一个是需要执行的被延迟的函数,第二个参数是延迟的具体时间。

var throttle = function (fn, interval){
    var _self = fn; // 保留需要被延迟执行的函数的引用
    var timer = null; // 定时器
    var firstTime = true; // 是否是第一次调用
    return function() {
        var args = arguments;
        var _me = this;
        if (firstTime) { // 如果是第一次调用,不需要延迟
            fn.apply(_me, args);
            return firstTime = false;
        }
        if(timer) { // 如果timer还没有执行,说明上一次的延迟执行还没完成,本次的调用去掉
            return false;
        }
        timer = setTimeout(function() { // 延迟一段时间执行
            clearTimeout(timer);
            timer = null;
            _self.apply(_me, args);
        }, interval || 500);
    }
}
window.onresize = throttle(function() {
    console.log(1);
}, 500);
复制代码

5.4 分时函数

某些函数确实是由用户主动调用,但是因为一些客观原因,这些函数会引起严重的性能问题。例如QQ的好友列表,有1000个好友。那么我们需要创建1000个好友节点并插入到页面中。那么短时间一次需要渲染插入1000个节,点,会让浏览器吃不消,就会出现浏览器的卡顿甚至假死。

var arr = []; // 创造1000个好友假数据
for (var i = 0; i < 1000; i++) {
    arr.push(i);
}

// 创建节点
var renderList = function(data) {
    var currentTime = (new Date()).valueOf();
    for (i = 0; i < data.length; i++) {
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild(div);
    }
    console.log(new Date().valueOf() - currentTime);
}

renderList(arr);

复制代码

这个问题的解决方案就是下面的timeThunk函数。让创建节点的工作分批进行,例如1秒创建1000个节点,修改为每个200毫秒创建8个节点。有三个参数:

  • 需要创建节点的数据
  • 封装创建节点的函数
  • 每一个批创建的节点数
var arr = []; // 创造1000个好友假数据
for (var i = 0; i < 1000; i++) {
    arr.push(i);
}

function timeThuck(data, fn, count) {
    var obj, t;
    var len = data.length;
    var  start = function() {
        for (var i = 0; i < Math.min(count || 1, data.length); i++) {
            fn(data.shift());
        }
    }
    return function() {
        var currentTime = (new Date()).valueOf();
        t = setInterval(function() {
            if (data.length === 0) {
				 console.log("time", (new Date()).valueOf() - currentTime);
                return clearInterval(t);
            }
            start();
        }, 200)
    }
}

function createElement(content) {
     var div = document.createElement('div');
        div.innerHTML = content;
        document.body.appendChild(div);
}

var renderList = timeThuck(arr, createElement, 8);
renderList();
复制代码

5.5 惰性加载函数

由于浏览器间的行为差异,在JavaScript的代码中,为了达到不同浏览器的兼容,经常需要写入大量判断语句,对于执行不同代码块,比如要实现一个在诸浏览器间通用的事件绑定函数addHandler,可以参考如下代码实现:

var addHandler = function(el, type, handler) {
    if ( window.addEventListener) {
        return el.addEventListener(type, handler);
    } else if (window.attachEvent) {
        return el.attachEvent('on' + type, handler);
    } else {
        return el['on' + type] = handler;
    }
}
复制代码

如上代码,在每次执行时都需要重新做条件判断,我们要如何才能做到在每个环境下只做一次判断呢?addHandler依然声明为一个普通函数,在第一次进入条件分支时,函数内部会重写这个函数,重写的就是符合当前环境的addHandler函数,当再次进入addHandler函数时,函数里不再存在条件分支语句:

 var addHandler = function(elem, type, handler) {
        if (window.addEventListener) {
            addHandler = function(elem, type, handler) {
                elem.addEventListener(type, handler); 
            }
        }else if (window.attachEvent) {
            addHandler = function(elem, type, handler) {
                elem.attachEvent('on' + type, handler);
            }
        }else {
            addHandler = function(elem, type, handler) {
                elem['on' + type] = handler;
            }
        }
        addHandler(elem, type, handler);
    };
复制代码