柯里化(Currying)这一术语源自英语中的“Currying”,它是一种将多参数函数转换为一系列接受单一参数函数的技术(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。下文将详细介绍柯里化函数的应用.
一、柯里化的特点
柯里化函数主要有3个特点,帮助我们优化函数:
参数复用(固定易变因素):
柯里化允许我们固定函数的部分参数,从而创建一个新的函数。这个新函数只需要接收剩余的参数即可。这种特性在需要多次使用相同参数调用函数时非常有用,因为它减少了重复代码,提高了代码的可重用性。
延迟执行:
通过柯里化,我们可以延迟函数的执行,直到所有必要的参数都被提供。这种延迟执行的特性使得我们可以更灵活地控制函数的调用时机,从而优化性能或实现特定的逻辑控制。
提前返回:
在某些情况下,我们可能希望在接收到部分参数后就立即返回结果,而不是等待所有参数都被接收。柯里化允许我们在函数内部根据已接收的参数进行条件判断,并提前返回结果。这种特性使得我们可以编写更加灵活和高效的函数。
二、柯里化的应用场景
柯里化可以在多个场景中应用:
利用柯里化制定约束条件,管控触发机制:
柯里化可以用于创建具有约束条件的函数。通过固定部分参数,我们可以创建一个新的函数,该函数在接收到剩余参数时才会执行特定的逻辑。这种机制可以用于控制函数的触发条件,从而避免不必要的计算或操作。
处理浏览器兼容(参数复用实现一次性判断):
在前端开发中,我们经常需要处理不同浏览器的兼容性问题。通过柯里化,我们可以将浏览器的类型或版本作为参数传递给函数,并创建一个新的函数来处理特定浏览器的兼容性问题。这样,我们就可以避免在每次调用函数时都进行浏览器类型的判断,从而提高代码的性能和可读性。
函数节流防抖(延迟执行):
函数节流和防抖是前端开发中常用的性能优化技术。通过柯里化,我们可以实现这些技术,从而控制函数的执行频率。例如,我们可以创建一个节流函数,该函数在接收到新的参数时会延迟执行原始函数,直到指定的时间间隔过去。同样地,我们也可以创建一个防抖函数,该函数在接收到新的参数时会取消之前的执行计划,并重新安排一个新的执行计划。
ES5前bind方法的实现:
在ES5之前,JavaScript没有内置的bind
方法。然而,我们可以使用柯里化来模拟bind
方法的行为。通过固定函数的上下文(this
值)和部分参数,我们可以创建一个新的函数,该函数在调用时会使用固定的上下文和参数。这种技术使得我们可以在不修改原始函数的情况下,为其添加特定的上下文和参数。
三、柯里化函数应用代码
这里还是举几个例子来说明一下:
柯里化求和函数
// 普通方式
var add1 = function(a, b, c){
return a + b + c;
}
// 柯里化
var add2 = function(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
这里每次传入参数都会返回一个新的函数,这样一直执行到最后一次返回a+b+c的值。但是这种实现还是有问题的,这里只有三个参数,如果哪天产品经理告诉我们需要改成100次?我们就重新写100次?这很明显不符合开闭原则,所以我们需要对函数进行一次修改。
var add = function() {
var _args = [];
return function() {
if(arguments.length === 0) {
return _args.reduce(function(a, b) {
return a + b;
})
}
[].push.apply(_args, arguments);
return arguments.callee;
}
}
var sum = add();
sum(100, 200)(300);
sum(400);
sum(); // 1000
我们通过判断下一次是否传进来参数来决定函数是否运行,如果继续传进了参数,那我们继续把参数都保存起来,等运行的时候全部一次性运行,这样我们就初步完成了一个柯里化的函数。
通用柯里化函数
这里只是一个求和的函数,如果换成求乘积呢?我们是不是又需要重新写一遍?仔细观察一下我们的add函数,如果我们将if里面的代码换成一个函数执行代码,是不是就可以变成一个通用函数了?
var curry = function(fn) {
var _args = [];
return function() {
if(arguments.length === 0) {
return fn.apply(fn, _args);
}
[].push.apply(_args, arguments);
return arguments.callee;
}
}
var multi = function() {
return [].reduce.call(arguments, function(a, b) {
return a + b;
})
}
var add = curry(multi);
add(100, 200, 300)(400);
add(1000);
add(); // 2000
在之前的方法上面,我们进行了扩展,这样我们就已经实现了一个比较通用的柯里化函数了。也许你想问,我不想每次都使用那个丑陋的括号结尾怎么办?
var curry = function(fn) {
var len = fn.length,
args = [];
return function() {
Array.prototype.push.apply(args, arguments)
var argsLen = args.length;
if(argsLen < len) {
return arguments.callee;
}
return fn.apply(fn, args);
}
}
var add = function(a, b, c) {
return a + b + c;
}
var adder = curry(add)
adder(1)(2)(3)
这里根据函数fn的参数数量进行判断,直到传入的数量等于fn函数需要的参数数量才会返回fn函数的最终运行结果,和上面那种方法原理其实是一样的,但是这两种方式都太依赖参数数量了。我在简书还看到别人的另一种递归实现方法,其实实现思路和我的差不多吧。
// 简单实现,参数只能从右到左传递
function createCurry(func, args) {
var arity = func.length;
var args = args || [];
return function() {
var _args = [].slice.call(arguments);
[].push.apply(_args, args);
// 如果参数个数小于最初的func.length,则递归调用,继续收集参数
if (_args.length < arity) {
return createCurry.call(this, func, _args);
}
// 参数收集完毕,则执行func
return func.apply(this, _args);
}
}
这里是对参数个数进行了计算,如果需要无限参数怎么办?比如下面这种场景。
add(1)(2)(3)(2);
add(1, 2, 3, 4, 5);
这里主要有一个知识点,那就是函数的隐式转换,涉及到toString和valueOf两个方法,如果直接对函数进行计算,那么会先把函数转换为字符串,之后再参与到计算中,利用这两个方法我们可以对函数进行修改。参考 前端手写面试题详细解答
var num = function() {
}
num.toString = num.valueOf = function() {
return 10;
}
var anonymousNum = (function() { // 10
return num;
}())
经过修改,我们的函数最终版是这样的。
var curry = function(fn) {
var func = function() {
var _args = [].slice.call(arguments, 0);
var func1 = function() {
[].push.apply(_args, arguments)
return func1;
}
func1.toString = func1.valueOf = function() {
return fn.apply(fn, _args);
}
return func1;
}
return func;
}
var add = function() {
return [].reduce.call(arguments, function(a, b) {
return a + b;
})
}
var adder = curry(add)
adder(1)(2)(3)
那么,柯里化究竟有什么用呢?
四、柯里化有什么用处?
预加载
在很多场景下,我们需要的函数参数很可能有一部分一样,这个时候再重复写就比较浪费了,我们提前加载好一部分参数,再传入剩下的参数,这里主要是利用了闭包的特性,通过闭包可以保持着原有的作用域。
var match = curry(function(what, str) {
return str.match(what);
});
match(/\s+/g, "hello world");
// [ ' ' ]
match(/\s+/g)("hello world");
// [ ' ' ]
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
上面例子中,使用hasSpaces函数来保存正则表达式规则,这样可以有效的实现参数的复用。
动态创建函数
这个其实也是一种惰性函数的思想,我们可以提前执行判断条件,通过闭包将其保存在有效的作用域中,来看一种我们平时写代码常见的场景。
var addEvent = function(el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};
在这个例子中,我们每次调用addEvent的时候都会重新进行if语句进行判断,但是实际上浏览器的条件不可能会变化,你判断一次和判断N次结果都是一样的,所以这个可以将判断条件提前加载。
var addEventHandler = function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
}
var addEvent = addEventHandler();
addEvent(document.body, "click", function() {}, false);
addEvent(document.getElementById("test"), "click", function() {}, false);
但是这样做还是有一种缺点,因为我们无法判断程序中是否使用了这个方法,但是依然不得不在文件顶部定义一下addEvent,这样其实浪费了资源,这里有一种更好的解决方法。
var addEvent = function(el, sType, fn, capture){
if (window.addEventListener) {
addEvent = function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
addEvent = function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
}
在addEvent函数里面对其重新赋值,这样既解决了每次运行都要判断的问题,又解决了必须在作用域顶部执行一次造成浪费的问题。
React
函数柯里化是不是可以扩展到更多场景,我想把函数换成react组件试试?我想到了高阶组件和redux的connect,这两个确实是将柯里化思想用到react里面的体现。我们想一想,如果把上面例子里面的函数换成组件,参数换成高阶函数呢?
var curry = function(fn) {
var func = function() {
var _args = [].slice.call(arguments, 0);
var func1 = function() {
[].push.apply(_args, arguments)
return func1;
}
func1.toString = func1.valueOf = function() {
return fn.apply(fn, _args);
}
return func1;
}
return func;
}
var hoc = function(WrappedComponent) {
return function() {
var len = arguments.length;
var NewComponent = WrappedComponent;
for (var i = 0; i < len; i++) {
NewComponent = arguments[i](NewComponent);
}
return NewComponent;
}
}
var MyComponent = hoc(PageList);
curry(MyComponent)(addStyle)(addLoading);
这个例子是对原来的PageList组件进行了扩展,给PageList加了样式和loading的功能,如果想加其他功能,可以继续在上面扩展(注意addStyle和addLoading都是高阶组件),但是写法真的很糟糕,一点都不coooooool,可以使用compose方法,underscore和loadsh这些库中已经提供了。
var enhance = compose(addLoading, addStyle);
enhance(MyComponent)
五、柯里化的缺点
柯里化是牺牲了部分性能来实现的,可能带来的性能损耗:
- 存取 arguments 对象要比存取命名参数要慢一些
- 老版本浏览器在 arguments.length 的实现相当慢(新版本浏览器忽略)
- fn.apply() 和 fn.call() 要比直接调用 fn() 慢
- 大量嵌套的作用域和闭包会带来开销,影响内存占用和作用域链查找速度
总结
其实关于柯里化的运用核心还是对函数闭包的灵活运用,深刻理解闭包和作用域后就可以写出很多灵活巧妙的方法。
延展阅读:
淘宝商家怎么设置定制商品的SKU?常见的定制商品sku违规有哪些?
如何用智能客服机器人提高产品复购率?电商商家怎么高效回复发货物流咨询?
咨询方案 获取更多方案详情