原文连接javascript
函数柯里化(Currying)是把接受多个参数的函数变成接受一个单一参数(最初的第一个参数)的函数,而且返回接受余下的参数且返回结果的新函数的技术。前端
函数柯里化并非JavaScript特有的。用笼统的话形容则是:减小函数参数个数的传递,并返回一个新的函数,这个新的函数可以处理旧函数剩下的参数。java
简单示例:git
// 计算两个数相加并返回计算结果的函数,接受两个参数a和b
function add (a, b) {
return a + b;
}
// 将函数柯里化
function curry (a) {
return function (b) {
return a + b;
}
}
// 应用函数柯里化
var _add = curry(1);
// 输出结果
console.log(_add(2)); // 3
// 比较结果
console.log(_add(2) === add(1, 2)); // true
// 另外一种比较
console.log(curry(1)(2) === add(1, 2)); // true
复制代码
这个是比较简单的函数柯里化过程,细心的同窗会发现,示例中的函数封装(柯里化)方式是具备较大的局限性的,不过它能给你们对函数柯里化有一种大概的认识。github
以上简单的示例可能并不能说什么,接下来,咱们将给出更详细的例子去配合理解。面试
详细示例:数组
假设有一个接受三个参数且求三个数之和的add
函数闭包
function add (a, b, c) {
// do something...
}
复制代码
而后通过咱们的柯里化(curry)函数封装后获得_add
函数app
var _add = curry(add);
复制代码
那么_add
是curry
封装后返回的柯里化函数,根据上述的定义,它可以处理add
的剩余参数。所以下面的函数调用都是等价的。函数
add(a, b, c) <=> _add(a, b, c) <=> _add(a, b)(c) <=> _add(a)(b,c) <=> _add(a)(b)(c)
复制代码
因此说,柯里化也叫作"部分求值"。
咱们将上面简单示例中的curry
函数,改为更加通用形式:
function curry(fn) {
// 记录原函数参数个数
var len= fn.length;
// 记录传参个数
var args = [].slice.call(arguments, 1);
// 返回新的函数
return function() {
// 保存新接收的参数为数组
var _args = [].slice.call(arguments);
// 将新旧两参数数组合并
[].unshift.apply(_args, args);
// 若是累积接收的参数个数少于原函数接受的参数,则递归调用
if (_args.length < len) {
return curry.call(this, fn, ..._args);
}
// 若个数知足要求,则返回原函数调用结果
return fn.apply(this, _args);
}
}
复制代码
示例应用:
function add (a, b, c) {
console.log(a + b + c)
return a + b + c;
}
var _add = curry(add);
_add(1, 2, 3); // 6
_add(1)(2, 3); // 6
_add(1, 2)(3); // 6
_add(1)(2)(3); // 6
var _add = curry(add, 1);
_add(2, 3); // 6
_add(2)(3); // 6
var _add = curry(add, 1, 2);
_add(3); // 6
var _add = curry(add, 1, 2, 3);
_add(); // 6
复制代码
这里代码逻辑也不难。咱们只需判断参数个数是否达到函数柯里化前的个数,若没有,则递归调用柯里化函数;若达到了,则执行函数,并返回执行后的结果。
有的同窗就苦恼了,函数柯里化,其实都最后还不是函数执行自身吗,为何还搞那么多花里胡哨的骚操做呢?函数柯里化确实把问题复杂化了,但同时提升了函数调用的自由度,这正是函数柯里化的核心所在。
请看一个常见的例子。
假设咱们有一个需求,须要验证用户输入是不是正确的手机号码,那么你们可能会这样封装函数:
function checkPhone (phoneNumber) {
return /^1[34578]\d{9}$/.test(phoneNumber);
}
复制代码
又假设咱们还有一个需求须要验证邮箱正确性,咱们可能又有以下封装:
function checkEmail(email) {
return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}
复制代码
这时候,产品经理又过来问咱们,能不能加上验证身份证号码、登录密码之类的。所以,咱们为了保持通用性,经常会有这样的封装:
function check (reg, str) {
return reg.test(str);
}
复制代码
这时,咱们就会有这样子的调用形式:
check(/^1[34578]\d{9}$/, '12345678910');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'checkson@gmail.com');
...
复制代码
若是要按照这种封装形式,咱们要调用屡次验证的话,须要屡次传入相同的正则匹配,而正则匹配每每是固定不变的。那么这个时候,咱们能够经过函数柯里化来让这些函数调用,变得优雅一些:
var _check = curry(check);
var checkPhone = _check(/^1[34578]\d{9}$/);
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
复制代码
最后的函数调用就会变得简洁明了了:
checkPhone('13912345678');
checkEmail('checkson@gmail.com');
复制代码
咱们能够发现,函数柯里化可以应对较复杂多变的业务需求,学好它是前端进阶的重点。
前端有一道关于柯里化的面试题,广为流传。
题目: 实现一个add方法,使如下等式成立
add(1)(2)(3) = 6
add(1, 2)(3)(4, 5) = 15
add(1, 2, 3)(4, 5)(6) = 21
add(1, 2) = 3
复制代码
这里须要补充一个重要的知识点:函数的隐式转换。当咱们直接将函数参与其余运算的时候,函数会默认调用toString
方法:
function fn () { return 1; }
console.log(fn + 1); // 输出结果为:function fn () { return 1; }1
复制代码
咱们能够重写函数的toString
方法。
function fn() { return 1; }
fn.toString = function() { return 2; }
console.log(fn + 1); // 3
复制代码
此外咱们还能够重写函数的valueOf
方法,获得一样的效果:
function fn() { return 1; }
fn.valueOf = function() { return 3; }
console.log(fn + 1); // 4
复制代码
当同时重写函数的toString
和valueOf
方法时,以valueOf
为准。
function fn() { return 1; }
fn.toString = function() { return 2; }
fn.valueOf = function() { return 3; }
console.log(fn + 1); // 4
复制代码
补充这个重要的知识点后,那么我们直接撸代码了:
function add () {
// 存储全部参数
var args = [].slice.call(arguments);
function adder () {
// 保存参数
args.push(...arguments);
// 重写valueOf方法
adder.valueOf = function () {
return args.reduce((a, b) => a + b);
}
// 递归返回adder函数
return adder;
}
// 返回adder函数调用
return adder();
}
复制代码
代码校验:
console.log(add(1)(2)(3) == 6) // true
console.log(add(1, 2)(3)(4, 5) == 15) // true
console.log(add(1, 2, 3)(4, 5)(6) == 21) // true
console.log(add(1, 2) == 3) // true
复制代码
这里代码的核心思想就是利用闭包来保存传入的全部参数和函数隐式转换。