18-柯里化函数/偏函数
几篇不错的文章:
- 1.JavaScript中的bind方法及其常见应用
- 2.理解JS里的偏函数与柯里化
- 3.javascirpt中偏函数的作用
- 4.前端开发者进阶之函数柯里化Currying
- 5.javascript中有趣的反柯里化
目前的理解:偏函数是柯里化函数的一种形态。做了一些参数的固定。
柯里化:是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
柯里化其实本身是固定一个可以预期的参数,并返回一个特定的函数,处理批特定的需求。这增加了函数的适用性,但同时也降低了函数的适用范围。
通用函数:
function currying(fn) {
// arguments为currying函数执行传入的参数
var slice = Array.prototype.slice,
// 去掉currying执行时传入的第一个参数->fn,拿到其他传入的所有参数
__args = slice.call(arguments, 1);
return function () {
// 这个arguments是在执行这个匿名函数时,传入的参数
var __inargs = slice.call(arguments);
// 把执行currying执行时传入的fn执行,并把执行两次时收集的入参合并起来,作为fn的参数
return fn.apply(null, __args.concat(__inargs));
};
}
function func(...args) {
return args.reduce((p, n) => p + n);
}
let f = currying(func, 1);
let res = f(2, 3, 4);
console.log(res);
1 提高适用性。
【通用函数】解决了兼容性问题,但同时也会带来,使用的不便利性,不同的应用场景往,要传递很多参数,以达到解决特定问题的目的。有时候应用中,同一种规则可能会反复使用,这就可能会造成代码的重复性。
看下面一个例子:
例子中,创建了一个map通用函数,用于适应不同的应用场景。显然,通用性不用怀疑。同时,例子中重复传入了相同的处理函数:square和dubble。
应用中这种可能会更多。当然,通用性的增强必然带来适用性的减弱。但是,我们依然可以在中间找到一种平衡。
我们利用柯里化改造一下:
// 柯里化通用方法
function currying(fn) {
var slice = Array.prototype.slice,
__args = slice.call(arguments, 1);
return function () {
var __inargs = slice.call(arguments);
return fn.apply(null, __args.concat(__inargs));
};
}
function dubbleFn(x) {
return (x *= 2);
}
function func(handler, data) {
return data.map(handler);
}
let f = currying(func, dubbleFn);
let res = f([1, 2, 3, 4]);
console.log(res); // [ 2, 4, 6, 8 ]
由此,可知柯里化不仅仅是提高了代码的合理性,更重的它突出一种思想---降低适用范围,提高适用性。
2.延迟执行
柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。
var curryFn = function (fn) {
var _args = [];
return function cb() {
// 递归结束条件
if (arguments.length == 0) {
return fn.apply(this, _args);
}
// 第一种写法 调用Array的push方法,借用apply,合并_args和传入的arguments
// apply可以将数组型参数扩展成参数列表,这样合并两个数组就可以直接传数组参数了。
/* 但是合并数组为什么不直接使用Array.prototype.concat()呢?
-因为concat不会改变原数组,concat会返回新数组,而上面apply这种写法直接改变数组_args。 */
Array.prototype.push.apply(_args, arguments);
// 第二种写法 :这样写都能看得懂
// _args.push(...arguments);
// 返回他自己,是个递归
return cb;
};
};
function func(...args) {
return args.reduce((p, n) => p + n);
}
let temp = curryFn(func);
console.log(temp(10)(1)(8)()); //19
3 固定易变因素(偏函数)
Function.prototype.bind = function (context) {
let _args = Array.prototype.slice.call(arguments, 1);
return () => {
let _inArgs = Array.prototype.slice.call(arguments);
return this.apply(context, _args.concat(_inArgs));
};
};
4.提前返回
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>柯里化函数-提前返回</title>
<style>
.box {
width: 200px;
height: 40px;
margin-bottom: 50px;
line-height: 40px;
background: rebeccapurple;
text-align: center;
}
</style>
</head>
<body>
<!-- normal -->
<div id="box" class="box"><h1>box1</h1></div>
<div id="box2" class="box"><h1>box2</h1></div>
<!-- curring -->
<div id="box3" class="box"><h1>box3</h1></div>
<div id="box4" class="box"><h1>box4</h1></div>
</body>
</html>
<script>
var oBox = document.querySelector("#box");
var oBox2 = document.querySelector("#box2");
var oBox3 = document.querySelector("#box3");
var oBox4 = document.querySelector("#box4");
function clickFn() {
console.log("hello~");
}
var addEvent = function (el, type, fn, capture) {
console.log("我是normal");
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(oBox, "click", clickFn, false);
addEvent(oBox2, "click", clickFn, false);
// -------------------------------------------------------------
var addEventCurring = (function () {
if (window.addEventListener) {
console.log("我被柯里化了");
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) {
el.attachEvent("on" + sType, function (e) {
fn.call(el, e);
});
};
}
})();
addEventCurring(oBox3, "click", clickFn, false);
addEventCurring(oBox4, "click", clickFn, false);
</script>
运行后查看控制台可以发现:
通过运行结果我们发现,当我们的 addEvent 和 addEventCurring 都被执行了两次,前者会走入if判断两次,后者只会进入到一次if判断;这就是被柯里化后的效果,可以细细品味。
高级柯里化实现
有一些柯里化的高级实现,可以实现更复杂功能:其返回一个包装器,它允许函数提供全部参数被正常调用,或返回偏函数。实现如下:
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {//如果参数大于等于函数参数,那么允许函数提供全部参数被正常调用
return func.apply(this, args);
} else {//提供参数小于函数参数,返回偏函数
return function pass(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
// 提供全部参数,正常调用
alert( curriedSum(1, 2, 3) ); // 6
// 返回偏函数包装器,并提供2、3参数
alert( curriedSum(1)(2,3) ); // 6
当我们运行时,有两个分支:
1、提供全部参数正常调用:如果传递args
数与原函数已经定义的参数个数一样或更长,那么直接调用。
2、获得偏函数:否则,不调用func
函数,返回另一个包装器,提供连接之前的参数一起做为新参数重新应用curried
。然后再次执行一个新调用,返回一个新偏函数(如果参数不够)或最终结果。
举例,让我们看sum(a, b, c)
会怎样,三个参数,所以sum.length=3;
如果调用curried(1)(2)(3):
(1)第一次调用curried(1)
,在词法环境中记住1,返回包装器pass;
(2)使用参数2调用包装器pass
:其带着前面的参数1,连接他们然后调用curried(1,2),
因为参数数量仍然小于3,返回包装器pass;
(3)再次使用参数3调用包装器pass,
带着之前的参数(1,2),
然后增加3
,并调用curried(1,2,3)
——最终有三个参数,传递给原始函数,然后参数个数相等,就直接调用func函数。
总结
1、当把已知函数的一些参数固定,结果函数被称为偏函数。通过使用bind
获得偏函数,也有其他方式实现。
用途:当我们不想一次一次重复相同的参数时,偏函数是很便捷的。如我们有send(from,to)
函数,如果from
总是相同的,可以使用偏函数简化调用。
2、柯里化是转换函数调用从f(a,b,c)
至f(a)(b)(c)
,Javascript通常既实现正常调用,也实现参数数量不足时的偏函数方式调用。
用途:(1)参数复用;(2)提前返回;(3)延迟计算或运行,参数随意设置。
偏函数与柯里化区别:
柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。
偏函数则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。
使用没有上下文的偏函数
bind可以实现偏函数应用,但是如果想固定一些参数,但不绑定this呢?
内置的bind
不允许这样,我们不能忽略上下文并跳转到参数。幸运的是,可以仅绑定参数partial
函数容易实现。如下:
(这句话的意思是,我们在使用bind的时候,必须还得传递一个context上下文,我们封装一个方法模拟bind实现的偏函数,但是不用传入bind的第一个参数上下文)
function partial(func, ...argsBound) {
return function(...args) {
return func.call(this, ...argsBound, ...args);
}
}
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// 偏函数,绑定第一个参数,say的time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
//调用新函数提供第二个参数phrase。
user.sayNow("Hello");
// [10:00], John : hello!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现