多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] # 柯里化到底是什么 > 维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 ~~~jsx // 普通的add函数 function add(x, y) { return x + y } // Currying后 function curryingAdd(x) { return function (y) { return x + y } } add(1, 2) // 3 curryingAdd(1)(2) // 3 ~~~ # Currying有哪些好处呢? ## 参数复用 ~~~jsx // 正常正则验证字符串 reg.test(txt) // 函数封装后 function check(reg, txt) { return reg.test(txt) } check(/\d+/g, 'test') //false check(/[a-z]+/g, 'test') //true // Currying后 function curryingCheck(reg) { return function(txt) { return reg.test(txt) } } var hasNumber = curryingCheck(/\d+/g) var hasLetter = curryingCheck(/[a-z]+/g) hasNumber('test1') // true hasNumber('testtest') // false hasLetter('21212') // false ~~~ 上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。 ## 提前确认 ~~~jsx var on = function(element, event, handler) { if (document.addEventListener) { if (element && event && handler) { element.addEventListener(event, handler, false); } } else { if (element && event && handler) { element.attachEvent('on' + event, handler); } } } var on = (function() { if (document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event && handler) { element.attachEvent('on' + event, handler); } }; } })(); //换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了 var on = function(isSupport, element, event, handler) { isSupport = isSupport || document.addEventListener; if (isSupport) { return element.addEventListener(event, handler, false); } else { return element.attachEvent('on' + event, handler); } } ~~~ 我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。 ## 延迟运行 ~~~jsx Function.prototype.bind = function (context) { var _this = this var args = Array.prototype.slice.call(arguments, 1) return function() { return _this.apply(context, args) } } ~~~ 像我们js中经常使用的bind,实现的机制就是Currying. ## 提高适用性 **通用函数**解决了兼容性问题,但同时也会再来,使用的不便利性,不同的应用场景往,要传递很多参数,以达到解决特定问题的目的。有时候应用中,同一种规则可能会反复使用,这就可能会造成代码的重复性。 ~~~ // 未柯里化前 function square(i) { return i * i; } function dubble(i) { return i * 2; } function map(handler, list) { return list.map(handler); } map(square, [1, 2, 3, 4, 5]); // 数组的每一项平方 map(square, [6, 7, 8, 9, 10]); map(dubble, [1, 2, 3, 4, 5]); // 数组的每一项加倍 map(dubble, [6, 7, 8, 9, 10]); ~~~ 同一规则重复使用,带来代码的重复性,因此可以使用上面的通用柯里化实现改造一下: ~~~ // 柯里化后 function square(i) { return i * i; } function dubble(i) { return i * 2; } function map(handler, ...list) { return list.map(handler); } var mapSQ = currying(map, square); mapSQ([1, 2, 3, 4, 5]); mapSQ([6, 7, 8, 9, 10]); var mapDB = currying(map, dubble); mapDB([1, 2, 3, 4, 5]); mapDB([6, 7, 8, 9, 10]); ~~~ 可以看到这里柯里化方法的使用和偏函数比较类似,顺便回顾一下偏函数~ **偏函数**是创建一个调用另外一个部分(参数或变量已预制的函数)的函数,函数可以根据传入的参数来生成一个真正执行的函数。比如: ~~~ const isType = function(type) { return function(obj) { return Object.prototype.toString.call(obj) === `[object ${type}]` } } const isString = isType('String') const isFunction = isType('Function') ~~~ 这样就用偏函数快速创建了一组判断对象类型的方法~ **偏函数**固定了函数的某个部分,通过传入的参数或者方法返回一个新的函数来接受剩余的参数,数量可能是一个也可能是多个 **柯里化**是把一个有n个参数的函数变成n个只有1个参数的函数,例如:`add = (x, y, z) => x + y + z`→`curryAdd = x => y => z => x + y + z` 当偏函数接受一个参数并且返回了一个只接受一个参数的函数,与两个接受一个参数的函数curry()()的柯里化函数,这时候两个概念类似。(个人理解不知道对不对) <br> <br> # 通用的封装方法 ~~~jsx // 初步封装 function currying(fn, ...rest1) { return function(...rest2) { return fn.apply(null, rest1.concat(rest2)) } } ~~~ 注意这里concat接受非数组元素参数将被当做调用者的一个元素传入 用它将一个sayHello函数柯里化试试: ~~~ function sayHello(name, age, fruit) { console.log(console.log(`我叫 ${name},我 ${age} 岁了, 我喜欢吃 ${fruit}`)) } const curryingShowMsg1 = currying(sayHello, '小明') curryingShowMsg1(22, '苹果') // 我叫 小明,我 22 岁了, 我喜欢吃 苹果 const curryingShowMsg2 = currying(sayHello, '小衰', 20) curryingShowMsg2('西瓜') // 我叫 小衰,我 20 岁了, 我喜欢吃 西瓜 ~~~ <br> <br> 以上柯里化函数已经能解决一般需求了,但是如果要多层的柯里化总不能不断地进行currying函数的嵌套吧,我们希望经过柯里化之后的函数每次只传递一个或者多个参数,那该怎么做呢: ~~~jsx function curryingHelper(fn, len) { const length = len || fn.length // 第一遍运行length是函数fn一共需要的参数个数,以后是剩余所需要的参数个数 return function(...rest) { return rest.length >= length // 检查是否传入了fn所需足够的参数 ? fn.apply(this, rest) : curryingHelper(currying.apply(this, [fn].concat(rest)), length - rest.length) // 在通用currying函数基础上 } } function sayHello(name, age, fruit) { console.log(`我叫 ${name},我 ${age} 岁了, 我喜欢吃 ${fruit}`) } const betterShowMsg = curryingHelper(sayHello) betterShowMsg('小衰', 20, '西瓜') // 我叫 小衰,我 20 岁了, 我喜欢吃 西瓜 betterShowMsg('小猪')(25, '南瓜') // 我叫 小猪,我 25 岁了, 我喜欢吃 南瓜 betterShowMsg('小明', 22)('倭瓜') // 我叫 小明,我 22 岁了, 我喜欢吃 倭瓜 betterShowMsg('小拽')(28)('冬瓜') // 我叫 小拽,我 28 岁了, 我喜欢吃 冬瓜 ~~~ 如此实现一个高阶的柯里化函数,使得柯里化一个函数的时候可以不用嵌套的currying,当然是因为把嵌套的地方放到了curryingHelper里面进行了 <br> <br> # 不限定参数 ~~~ function sumWithES6(...rest) { var _args = rest; var _adder = function (...innerRest) { _args.push(...innerRest); // 这里使用的是ES6数组的解构 return _adder; }; _adder.toString = function () { let sum = _args.reduce(function (a, b) { return a + b; }); return sum; }; return _adder; } console.log(sumWithES6(1)(2)(3)); // 6 ~~~ # 先指定参数个数 ~~~ function total(argNum) { return function sum() { var args = Array.from(arguments) var restArgs = argNum - args.length var that = this if (restArgs) { return function () { return sum.apply(that, args.concat(Array.from(arguments))) } } else { return args.reduce((cur, next) => { return cur + next }, 0) } } } var two = total(2) console.log(two(1,2)) console.log(two(1)(4)) var three = total(3) console.log(three(4,5,6)) console.log(three(1,2)(5)) console.log(three(1)(4)(7)) ~~~ # 参考资料 [JS中的柯里化](https://segmentfault.com/a/1190000012769779) [详解JS函数柯里化](https://www.jianshu.com/p/2975c25e4d71) [sum(1,2)(3)与函数柯里化](http://yimiao.space/2021/02/01/sum-1-2-3-%E4%B8%8E%E5%87%BD%E6%95%B0%E6%9F%AF%E9%87%8C%E5%8C%96/)