在 JavaScript 中,柯里化和反柯里化是高阶函数的一种应用,在这以前咱们应该清楚什么是高阶函数,通俗的说,函数能够做为参数传递到函数中,这个做为参数的函数叫回调函数,而拥有这个参数的函数就是高阶函数,回调函数在高阶函数中调用并传递相应的参数,在高阶函数执行时,因为回调函数的内部逻辑不一样,高阶函数的执行结果也不一样,很是灵活,也被叫作函数式编程。正则表达式
在 JavaScript 中,函数柯里化是函数式编程的重要思想,也是高阶函数中一个重要的应用,其含义是给函数分步传递参数,每次传递部分参数,并返回一个更具体的函数接收剩下的参数,这中间可嵌套多层这样的接收部分参数的函数,直至返回最后结果。编程
// 柯里化拆分 // 原函数 function add(a, b, c) { return a + b + c; } // 柯里化函数 function addCurrying(a) { return function (b) { return function (c) { return a + b + c; } } } // 调用原函数 add(1, 2, 3); // 6 // 调用柯里化函数 addCurrying(1)(2)(3) // 6
被柯里化的函数 addCurrying
每次的返回值都为一个函数,并使用下一个参数做为形参,直到三个参数都被传入后,返回的最后一个函数内部执行求和操做,实际上是充分的利用了闭包的特性来实现的。数组
上面的柯里化函数没涉及到高阶函数,也不具有通用性,没法转换形参个数任意或未知的函数,咱们接下来封装一个通用的柯里化转换函数,能够将任意函数转换成柯里化。闭包
// 柯里化通用式 ES5 function currying(func, args) { // 形参个数 var arity = func.length; // 上一次传入的参数 var args = args || []; return function () { // 将参数转化为数组 var _args = [].slice.call(arguments); // 将上次的参数与当前参数进行组合并修正传参顺序 Array.prototype.unshift.apply(_args, args); // 若是参数不够,返回闭包函数继续收集参数 if(_args.length < arity) { return currying.call(null, func, _args); } // 参数够了则直接执行被转化的函数 return func.apply(null, _args); } }
上面主要使用的是 ES5 的语法来实现,大量的使用了 call
和 apply
,下面咱们经过 ES6 的方式实现功能彻底相同的柯里化转换通用式。app
// 柯里化通用式 ES6 function currying(func, args = []) { let arity = func.length; return function (..._args) { _args.unshift(...args); if(_args.length < arity) { return currying(func, _args); } return func(..._args); } }
函数 currying
算是比较高级的转换柯里化的通用式,能够随意拆分参数,假设一个被转换的函数有多个形参,咱们能够在任意环节传入任意个数的参数进行拆分,举一个例子,假如 5
个参数,第一次能够传入 2
个,第二次能够传入 1
个, 第三次能够传入剩下的,也有其余的多种传参和拆分方案,由于在 currying
内部收集参数的同时按照被转换函数的形参顺序进行了更正。函数式编程
柯里化的一个很大的好处是能够帮助咱们基于一个被转换函数,经过对参数的拆分实现不一样功能的函数,以下面的例子。函数
// 柯里化通用式应用 —— 普通函数 // 被转换函数,用于检测传入的字符串是否符合正则表达式 function checkFun(reg, str) { return reg.test(str); } // 转换柯里化 const check = currying(checkFun); // 产生新的功能函数 const checkPhone = check(/^1[34578]\d{9}$/); const checkEmail = check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
上面的例子根据一个被转换的函数经过转换变成柯里化函数,并用 check
变量接收,之后每次调用 check
传递不一样的正则就会产生一个检测不一样类型字符串的功能函数。this
这种使用方式一样适用于被转换函数是高阶函数的状况,好比下面的例子。spa
// 柯里化通用式应用 —— 高阶函数 // 被转换函数,按照传入的回调函数对传入的数组进行映射 function mapFun(func, array) { return array.map(func); } // 转换柯里化 const getNewArray = currying(mapFun); // 产生新的功能函数 const createPercentArr = getNewArray(item => `${item * 100}%`); const createDoubleArr = getNewArray(item => item * 2); // 使用新的功能函数 let arr = [1, 2, 3, 4, 5]; let percentArr = createPercentArr(arr); // ['100%', '200%', '300%', '400%', '500%',] let doubleArr = createDoubleArr(arr); // [2, 4, 6, 8, 10]
bind
方法是常用的一个方法,它的做用是帮咱们将调用 bind
函数内部的上下文对象 this
替换成咱们传递的第一个参数,并将后面其余的参数做为调用 bind
函数的参数。prototype
// bind 方法原理模拟 // bind 方法的模拟 Function.prototype.bind = function (context) { var self = this; var args = [].slice.call(arguments, 1); return function () { return self.apply(context, args); } }
经过上面代码能够看出,其实 bind
方法就是一个柯里化转换函数,将调用 bind
方法的函数进行转换,即经过闭包返回一个柯里化函数,执行该柯里化函数的时候,借用 apply
将调用 bind
的函数的执行上下文转换成了 context
并执行,只是这个转换函数没有那么复杂,没有进行参数拆分,而是函数在调用的时候传入了全部的参数。
反柯里化的思想与柯里化正好相反,若是说柯里化的过程是将函数拆分红功能更具体化的函数,那反柯里化的做用则在于扩大函数的适用性,使原本做为特定对象所拥有的功能函数能够被任意对象所使用。
反柯里化通用式的参数为一个但愿能够被其余对象调用的方法或函数,经过调用通用式返回一个函数,这个函数的第一个参数为要执行方法的对象,后面的参数为执行这个方法时须要传递的参数。
// 反柯里化通用式 ES5 function uncurring(fn) { return function () { // 取出要执行 fn 方法的对象,同时从 arguments 中删除 var obj = [].shift.call(arguments); return fn.apply(obj, arguments); } }
// 反柯里化通用式 ES6 function uncurring(fn) { return function (...args) { return fn.call(...args); } }
下面咱们经过一个例子来感觉一下反柯里化的应用。
// 反柯里化通用式应用 // 构造函数 F function F() {} // 拼接属性值的方法 F.prototype.concatProps = function () { let args = Array.from(arguments); return args.reduce((prev, next) => `${this[prev]}&${this[next]}`); } // 使用 concatProps 的对象 let obj = { name: "Panda", age: 16 }; // 使用反柯里化进行转化 const concatProps = uncurring(F.prototype.concatProps); concatProps(obj, "name", "age"); // Panda&16
反柯里化还有另一个应用,用来代替直接使用 call
和 apply
,好比检测数据类型的 Object.prototype.toString
等方法,以往咱们使用时是在这个方法后面直接调用 call
更改上下文并传参,若是项目中多处须要对不一样的数据类型进行验证是很麻的,常规的解决方案是封装成一个检测数据类型的模块。
// 检测数据类型常规方案 function checkType(val) { return Object.prototype.toString.call(val); }
若是须要这样封装的功能不少就麻烦了,代码量也会随之增大,其实咱们也可使用另外一种解决方案,就是利用反柯里化通用式将这个函数转换并将返回的函数用变量接收,这样咱们只须要封装一个 uncurring
通用式就能够了。
// 反柯里化建立检测类型函数 const checkType = uncurring(Object.prototype.toString); checkType(1); // [object Number] checkType("hello"); // [object String] checkType(true); // [object Boolean]
在 JavaScript 咱们常用面向对象的编程方式,在两个类或构造函数之间创建联系实现继承,若是咱们对继承的需求仅仅是但愿一个构造函数的实例可以使用另外一个构造函数原型上的方法,那进行繁琐的继承很浪费,简单的继承父子类的关系又不那么的优雅,还不如之间不存在联系。
// 将反柯里化方法扩展到函数原型 Function.prototype.uncurring = function () { var self = this; return function () { return Function.prototype.call.apply(self, arguments); } }
以前的问题经过上面给函数扩展的 uncurring
方法彻底获得了解决,好比下面的例子。
// 函数应用反柯里化原型方法 // 构造函数 function F() {} F.prototype.sayHi = function () { return "I'm " + this.name + ", " + this.age + " years old."; } // 但愿 sayHi 方法被任何对象使用 sayHi = F.prototype.sayHi.uncurring(); sayHi({ name: "Panda", age: 20}); // I'm Panda, 20 years old.
在 Function
的原型对象上扩展的 uncurring
中,难点是理解 Function.prototype.call.apply
,咱们知道在 call
的源码逻辑中 this
指的是调用它的函数,在 call
内部用第一个参数替换了这个函数中的 this
,其他做为形参执行了函数。
而在 Function.prototype.call.apply
中 apply
的第一个参数更换了 call
中的 this
,这个用于更换 this
的就是例子中调用 uncurring
的方法 F.prototype.sayHi
,因此等同于 F.prototype.sayHi.call
,arguments
内的参数会传入 call
中,而 arguments
的第一项正是用于修改 F.prototype.sayHi
中 this
的对象。
看到这里你应该对柯里化和反柯里化有了一个初步的认识了,但要熟练的运用在开发中,还须要咱们更深刻的去了解它们内在的含义。