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);consofunc(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); };复制代码