函数式编程是一种编程风格,它尝试将函数作为参数传递(回调)并返回没有副作用的函数。因此带来了一些其他东西,比如纯函数、柯里化、高阶函数。 这里我们主要讨论函数柯里化的实现与应用场景。
什么是柯里化 柯里化是函数式编程中的一个过程,我们可以将具有多个参数的函数转换为一系列嵌套函数。它返回一个新函数,该函数能接受下一个参数。
柯里化是将具有多元数的函数变成具有较少元数的函数的过程 - Kristina Brainwave
举个例子:
1 2 3 4 5 function multiply (a, b, c ) { return a * b * c; } multiply(1 , 2 , 3 );
现在我们创建一个柯里化函数版本:
1 2 3 4 5 6 7 8 function multiply (a ) { return (b ) => { return (c ) => { return a * b * c; }; }; } multiply(1 )(2 )(3 );
我们已经把 multiply(1,2,3) 函数调用变成了 multiply(1)(2)(3)多个函数的调用。
而对于 Javascript 语言来说,我们通常说的柯里化函数的概念,与数学和计算机科学中的柯里化的概念并不完全一样。 在数学和计算机科学中的柯里化函数,一次只能传递一个参数; 而我们 Javascript 实际应用中的柯里化函数,可以传递一个或多个参数。 来看这个例子。
1 2 3 4 5 6 7 8 9 10 11 function fn (a, b, c, d, e ) { console .log(a, b, c, d, e); } let _fn = curry(fn);_fn(1 , 2 , 3 , 4 , 5 ); _fn(1 )(2 )(3 , 4 , 5 ); _fn(1 , 2 )(3 , 4 )(5 ); _fn(1 )(2 )(3 )(4 )(5 );
对于已经柯里化后的 _fn 函数来说,当接收的参数数量与原函数的形参数量相同时,执行原函数; 当接收的参数数量小于原函数的形参数量时,返回一个函数用于接收剩余的参数,直至接收的参数数量与形参数量一致,执行原函数。 当我们知道柯里化是什么了的时候,我们来看看柯里化到底有什么用?
用途 柯里化实际是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度,而这里对于函数参数的自由处理,正是柯里化的核心所在。 柯里化本质上是降低通用性,提高适用性。来看一个例子: 我们工作中会遇到各种需要通过正则检验的需求,比如校验电话号码、校验邮箱、校验身份证号、校验密码等, 这时我们会封装一个通用函数 checkByRegExp ,接收两个参数,校验的正则对象和待校验的字符串:
1 2 3 4 5 6 function checkByRegExp (regExp, string ) { return regExp.test(string); } checkByRegExp(/^1\d{10}$/ , "18642838455" ); checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/ , "test@163.com" );
我们每次进行校验的时候都需要输入一串正则,再校验同一类型的数据时,相同的正则我们需要写多次, 这就导致我们在使用的时候效率低下,并且由于 checkByRegExp 函数本身是一个工具函数并没有任何意义, 一段时间后我们重新来看这些代码时,如果没有注释,我们必须通过检查正则的内容, 我们才能知道我们校验的是电话号码还是邮箱,还是别的什么。 此时,我们可以借助柯里化对 checkByRegExp 函数进行封装,以简化代码书写,提高代码可读性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let _check = curry(checkByRegExp);let checkCellPhone = _check(/^1\d{10}$/ );let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/ );checkCellPhone("18642838455" ); checkCellPhone("13109840560" ); checkCellPhone("13204061212" ); checkEmail("test@163.com" ); checkEmail("test@qq.com" ); checkEmail("test@gmail.com" );
经过柯里化后,我们生成了两个函数 checkCellPhone 和 checkEmail, checkCellPhone 函数只能验证传入的字符串是否是电话号码, checkEmail 函数只能验证传入的字符串是否是邮箱, 它们与 原函数 checkByRegExp 相比,从功能上通用性降低了,但适用性提升了。 柯里化的这种用途可以被理解为:参数复用。
我们再来看一个例子,比如我们有这样一段数据:
1 var person = [{ name : "lilei" }, { name : "hanmeimei" }];
如果我们要获取那么属性,我们可以这样:
1 var names = person.map((item ) => item.name);
如果我们有 curry 函数:
1 2 3 4 5 var prop = curry(function (key, obj ) { return obj[key]; }); var name = person.map(prop("name" ));
这里我们为了获取 name 属性编写一个 prop 函数,你可能会觉得太麻烦了。 但是要注意,prop 函数编写一次后,以后可以多次使用,我们在考虑代码复杂的的时候,是可以将 prop 函数的实现去掉的,实际上代码从原本的三行精简成了一行。
柯里化工具函数封装 常见 curry 函数的实现为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var curry = function (fn ) { var args = [].slice.call(arguments ); return function ( ) { var newArgs = args.concat([].slice.call(arguments )); return fn.apply(this , newArgs); }; }; function add (a, b ) { return a + b; } var addCurry = curry(add, 1 , 2 );addCurry(); var addCurry = curry(add, 1 );addCurry(2 ); var addCurry = curry(add);addCurry(1 , 2 );
已经有柯里化的感觉了,但是还没有达到要求,不过我们可以把这个函数用作辅助函数,帮助我们写真正的 curry 函数。
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 27 28 29 30 31 32 function sub_curry (fn ) { var args = [].slice.call(arguments , 1 ); return function ( ) { return fn.apply(this , args.concat([].slice.call(arguments ))); }; } function curry (fn, length ) { length = length || fn.length; var slice = Array .prototype.slice; return function ( ) { if (arguments .length < length) { var combined = [fn].concat(slice.call(arguments )); var sub_fn = sub_curry.apply(this , combined); return curry(sub_fn, length - arguments .length); } else { return fn.apply(this , arguments ); } }; } var fn = curry(function (a, b, c ) { return [a, b, c]; }); fn("a" , "b" , "c" ); fn("a" , "b" )("c" ); fn("a" )("b" )("c" ); fn("a" )("b" , "c" );
如果上面的实现不好理解的话,我们换一种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function sun_curry (fn, ...args ) { return function (...params ) { return fn.apply(this , [...args, ...params]); }; } function curry (fn, length = fn.length ) { return function (...params ) { if (params.length < length) { var sub_fn = sub_curry.apply(this , [fn, ...params]); return curry(sub_fn, length - params.length); } else { fn.apply(this , params); } }; }
sun_curry 方法的作用在与接收一个函数和若干参数,然后返回一个函数,该函数能接收后续若干参数。 curry 方法的作用就是递归调用辅助函数 sub_curry,实现函数柯里化。
参考文献 https://juejin.cn/post/6844903882208837645