JS函数柯里化
原网址:https://www.jianshu.com/p/2975c25e4d71
首先看看柯里化到底是什么
维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。
1 // 普通的add函数 2 function add(x, y) { 3 return x + y 4 } 5 6 // Currying后 7 function curryingAdd(x) { 8 return function (y) { 9 return x + y 10 } 11 } 12 13 add(1, 2) // 3 14 curryingAdd(1)(2) // 3
实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
来列一列Currying有哪些好处呢?
1. 参数复用
1 // 正常正则验证字符串 reg.test(txt) 2 3 // 函数封装后 4 function check(reg, txt) { 5 return reg.test(txt) 6 } 7 8 check(/\d+/g, 'test') //false 9 check(/[a-z]+/g, 'test') //true 10 11 // Currying后 12 function curryingCheck(reg) { 13 return function(txt) { 14 return reg.test(txt) 15 } 16 } 17 18 var hasNumber = curryingCheck(/\d+/g) 19 var hasLetter = curryingCheck(/[a-z]+/g) 20 21 hasNumber('test1') // true 22 hasNumber('testtest') // false 23 hasLetter('21212') // false
上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。
2. 提前返回
1 var on = function(element, event, handler) { 2 if (document.addEventListener) { 3 if (element && event && handler) { 4 element.addEventListener(event, handler, false); 5 } 6 } else { 7 if (element && event && handler) { 8 element.attachEvent('on' + event, handler); 9 } 10 } 11 } 12 13 var on = (function() { 14 if (document.addEventListener) { 15 return function(element, event, handler) { 16 if (element && event && handler) { 17 element.addEventListener(event, handler, false); 18 } 19 }; 20 } else { 21 return function(element, event, handler) { 22 if (element && event && handler) { 23 element.attachEvent('on' + event, handler); 24 } 25 }; 26 } 27 })(); 28 29 //换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了 30 var on = function(isSupport, element, event, handler) { 31 isSupport = isSupport || document.addEventListener; 32 if (isSupport) { 33 return element.addEventListener(event, handler, false); 34 } else { 35 return element.attachEvent('on' + event, handler); 36 } 37 }
我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。
3. 延迟运行
1 Function.prototype.bind = function (context) { 2 var _this = this 3 var args = Array.prototype.slice.call(arguments, 1) 4 5 return function() { 6 return _this.apply(context, args) 7 } 8 }
像我们js中经常使用的bind,实现的机制就是Currying.
有没有什么通用的封装方法?
1 // 初步封装 2 var currying = function(fn) { 3 // args 获取第一个方法内的全部参数 4 var args = Array.prototype.slice.call(arguments, 1) 5 return function() { 6 // 将后面方法里的全部参数和args进行合并 7 var newArgs = args.concat(Array.prototype.slice.call(arguments)) 8 // 把合并后的参数通过apply作为fn的参数并执行 9 return fn.apply(this, newArgs) 10 } 11 }
这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。
但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。
1 // 支持多参数传递 2 function progressCurrying(fn, args) { 3 4 var _this = this 5 var len = fn.length; 6 var args = args || []; 7 8 return function() { 9 var _args = Array.prototype.slice.call(arguments); 10 Array.prototype.push.apply(args, _args); 11 12 // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数 13 if (_args.length < len) { 14 return progressCurrying.call(_this, fn, _args); 15 } 16 17 // 参数收集完毕,则执行fn 18 return fn.apply(this, _args); 19 } 20 }
这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。
好处说完了,通用方法也有了,让我们来关注下curry的性能
curry的一些性能问题你只要知道下面四点就差不多了:
- 存取arguments对象通常要比存取命名参数要慢一点
- 一些老版本的浏览器在arguments.length的实现上是相当慢的
- 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
- 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,所以curry是可以直接放心的使用。
最后再扩展一道经典面试题
1 // 实现一个add方法,使计算结果能够满足如下预期: 2 add(1)(2)(3) = 6; 3 add(1, 2, 3)(4) = 10; 4 add(1)(2)(3)(4)(5) = 15; 5 6 function add() { 7 // 第一次执行时,定义一个数组专门用来存储所有的参数 8 var _args = Array.prototype.slice.call(arguments); 9 10 // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值 11 var _adder = function() { 12 _args.push(...arguments); 13 return _adder; 14 }; 15 16 // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 17 _adder.toString = function () { 18 return _args.reduce(function (a, b) { 19 return a + b; 20 }); 21 } 22 return _adder; 23 } 24 25 add(1)(2)(3) // 6 26 add(1, 2, 3)(4) // 10 27 add(1)(2)(3)(4)(5) // 15 28 add(2, 6)(1) // 9