高阶函数是指满足下列条件之一的函数:
函数可以作为参数进行传递 函数可以作为返回值进行输出 JavaScript 语言中的函数显然满足高阶函数的条件,在实际开发中,无论是将函数当做参数传递,还是让函数的执行结果返回给另外一个函数,这两种情形都有很多应用场景。
函数作为参数传递 把函数当做参数进行传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来就可以分离业务代码中变化和不变的部分。
在 ajax
异步请求的应用中,回调函数的使用非常频繁。当我们想在 ajax
请求返回之后做些事情,但又并不知道请求返回的确切时间时,最常见的方案是把 callback
函数当做参数传入发起的 ajax
请求的方法中,待请求完成之后执行 callback
函数:
1 2 3 4 5 6 7 var getUserInfo = function (userId, callback ) { $.ajax ("http://webape.net/getUserInfo?" + userId, function (data ) { if (typeof callback === "function" ) { callback (data); } }); };
再来看一个例子,假设有这样一个需求,需要创建 100
个 div
元素,同时把他们隐藏起来,那么可以看到下面这种实现:
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
接受一个函数作为参数,这个函数里面封装了数组元素的排序顺序。我们的目的是对数组进行排序,这是不变的部分;但用什么规则去排序这是可变的部分。
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 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 ([]); Type .isString ("" );
下面是一个单例模式的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 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);
高阶函数实现 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 25 26 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 ();
高阶函数的其他应用
currying
的概念最早由俄国数学家 Moses Schoofinkel
发明,而后由著名的数理逻辑学家 Haskell Curry
将其丰富和发展,currying
由此得名。
currying
又称部分求值。这里我们讨论的是函数柯里化( function currying
)。一个柯里化函数首先会接受一些参数,接受了这些参数会后,该函数并不会立即求值,而是继续返回另一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正求值的时候,之前传入的所有参数都会被一次性用于求值。来看一个例子: 假设我们要编写一个计算每月开销的函数。在每天结束之前,我们都要记录几天花掉了多少钱。
1 2 3 4 5 6 7 8 9 10 11 var monthCost = 0 ;var cost = function (money ) { monthCost += money; }; cost (100 );第一天; cost (200 );第二天; cost (300 );第三天; console .log (monthCost);
通过这段代码,我们可以看到每天都花了多少钱,但是如果我们只想知道每个月的消费如何的话,那就没必要计算每天的花费了。
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 ());
接下来编写一个通用的柯里化函数:
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 ());
uncurrying
是反柯里化,大概意思是扩大函数的应用范围,将本来只有特定对象才能使用的方法,扩展到更多的对象。 比如我们常常让类数组对象去借用 Array.prototype
的方法:
1 2 3 4 (function ( ) { Array .prototype .push .call (arguments , 4 ); console .log (arguments ); })(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 ); var first = Array .shift (obj);console .log (first); console .log (obj); Array .forEach (obj, function (i, n ) { console .log (n); });
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 );
上面我们介绍了一种解决函数被频繁调用的方法。但是有时候,用户确实有这种需求,比如需要在短时间内把 1000
个 qq 好友渲染到列表上,这个时候就可能会很卡。但是如果把 1000ms
创建 1000
个节点,改成每 200ms
创建 8
个节点。这个时候就避免这种问题。 分时函数接受 3
个参数:第一个是创建节点的时候需要用到的数据,第二个是封装了创建节点的函数,第三个是每一批创建的节点数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 11 12 13 14 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); };