基础概念
当一个函数有多个参数的时候,先传递一部分参数调用他(这部分参数以后永远不变),然后返回一个新的函数接受剩余的参数,返回结果;
简言之就是:多变量函数拆解为单变量的多个函数的依次调用;
可以干嘛呢?
可以利用它来实现对函数参数的缓存,降低函数粒度,把多元函数转换成一元函数,实现函数的组合,产生更强大的功能
核心流程分析
就是利用闭包和递归调用,可以形成一个不销毁的私有作用域,把预先处理的内容放到不销毁的作用域里面,返回一个函数供以后调用;
举个例子:
比如我们有一个判断用户年龄是否大于某个值的函数
// 普通的纯函数
function checkAge (min, age) {
return age >= min
}
// 普通调用
console.log(checkAge(18, 20)) //true
console.log(checkAge(18, 24)) //true
console.log(checkAge(60, 30)) //false
可能需要经常判断用户是否成年(大于18岁),为了减少代码重复,所以改造如下
// 柯里化后的函数
function checkAge (min) {
return function (age) {
return age >= min
}
}
const checkAge18 = checkAge(18)
const checkAge60 = checkAge(60)
console.log(checkAge18(20)) //true
console.log(checkAge18(24)) //true
console.log(checkAge60(30)) //false
以上就是一个针对checkAge函数的柯里化改造,他的自由度很低,因此需要封装一个通用的柯里化函数;
实现思路
首先,我们通过调用lodash提供的柯里化函数(curry)来了解一下如何使用,并且分析一下实现思路
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
// 定义一个柯里化函数
const curried = _.curry(getSum)
// 如果输入了全部的参数,则立即返回结果
console.log(curried(1, 2, 3)) // 6
//如果传入了部分的参数,此时它会返回当前函数,并且等待接收getSum中的剩余参数
console.log(curried(1)(2, 3)) // 6
console.log(curried(1, 2)(3)) // 6
通过以上可以看出,柯里化函数的运行过程其实是一个参数的收集过程,将每一次传入的参数收集起来,在最后统一处理
所以,实现思路:
- 调用curry,传递一个函数,然后需要返回一个柯里化函数(curried)
- 如果调用curried传递的参数和getSum参数个数相同,就立即执行并返回结果;如果调用curried传递的是部分参数,那么需要返回一个新函数,等待接受getSum其他参数
具体实现如下:
function curry(func) {
return function curriedFn(...args) {
// 若实参的个数小于形参的个数
if (args.length < func.length) {
return function () {
// 等待传递的剩余参数
// 第一部分参数在args里面,第二部分参数在arguments里面
return curriedFn(...args.concat(...arguments));
};
}
// 如果实参大于等于形参的个数,立即执行并返回结果
// args是剩余参数
return func(...args);
};
}
注意:这里有个细节,就是要柯理化的函数不能有默认值,否则该函数的length属性将失真;
将造成结果提前返回或者报错
如下:
该技术的优缺点
上面费那么大劲封装,到底有什么好处呢?
优点:
参数复用;参考上面的checkAge函数,把18这个参数缓存起来,多个地方用到18的就可以直接调用
将多元函数比变成一元函数,然后组合函数产生更强大功能
延迟运行;像经常使用的bind,就是基于柯里化实现的;
Function.prototype.bind = function (context) {
var _this = this
var args = Array.prototype.slice.call(arguments, 1)
return function() {
return _this.apply(context, args)
}
}
那缺点也显然易见:
- 使用了大量的闭包,内存得不到释放,容易造成内存泄漏
对比传统的函数调用,则不会产生闭包,使用完即可释放
其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,只要注意闭包的内存释放即可放心使用。
面试题
一)
// 实现一个add方法,使计算结果能够满足如下预期:
add(1,2,3) = 6;
add(1,2)(3) = 6;
add(1)(2)(3) = 6;
这个题目是想让add函数执行后,返回一个能够继续执行的函数,最终计算出所有参数的和,重点在于每次接受的参数可以有一个,也可以有多个(add接受的参数个数固定);
答案如下:
function curry(func) {
return function curriedFn(...args) {
// 若实参的个数小于形参的个数
if (args.length < func.length) {
return function () {
// 等待传递的剩余参数
// 第一部分参数在args里面,第二部分参数在arguments里面
return curriedFn(...args.concat(...arguments));
};
}
// 如果实参大于等于形参的个数,立即执行并返回结果
// args是剩余参数
return func(...args);
};
}
function add(a,b,c){
return a+b+c;
}
const newFn = curry(add)
newFn(1)(2)(3) //6
newFn(1,2)(3) //6
newFn(1,2,3) //6
上述考题是参数固定:也就是add已知参数就是3个;那参数不固定的,如何解决呢?请看第2题
二)
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
这个题目相较于第1题,它的难点在于add的参数不固定;所以要继续优化;
先来看下面两种解法
解法1.
// 柯里化写法
function sum(...arr) {
return arr.reduce((per, next) => {
return per + next;
}, 0);
}
function curry(fn) {
let args = [];
return function curried(...res) {
if (res.length) {
args = [...args, ...res];
return curried;
} else {
return fn.apply(this, args);
}
};
}
let add = curry(sum);
console.log(add(1)(2)(3)()); //6
解法2.
//toString 写法
function curry(a) {
function curried(item) {
a += item;
return curried;
}
curried.toString = function () {
return a;
};
return curried;
}
console.log(curry(1)(2)(3).toString()); //6
以上两种方式虽然都能实现,但是解法1需要最后再调用一次,而解法2需要多调用一个转换函数;
都有点勉强,不太符合考题调用方式;
那来看最后一种实现方式:
解法3.
function add(...args) {
let final = [...args];
setTimeout(() => {
console.log(final.reduce((sum, cur) => sum + cur));
}, 0);
const inner = function (...args) {
final = [...final, ...args];
return inner;
};
return inner;
}
console.log(add(1)(2)(3)); //6
这个方法利用了异步编程,setTimeout中的内容延迟执行,算是个奇淫技巧,但终归是符合了考题的调用方法;
具体使用哪种,还要看面试官想考什么?
如果是考柯里化知识点,那就选解法1;
如果必须按照题目方式调用,那只能选择解法3