前言
柯里化是js中一个常用的概念,接下来我们来尝试写出一个 curry 函数,接收一个函数然后返回该函数的柯里化
const join = (a, b, c) => {
return `${a}_${b}_${c}`
}
const curriedJoin = curry(join)
curriedJoin(1, 2, 3) // '1_2_3'
curriedJoin(1)(2, 3) // '1_2_3'
curriedJoin(1, 2)(3) // '1_2_3'
实现
//答案1
function curry(func) {
return function innerFunc(...args) {
//func为原函数,innerFunc为新函数
//如果新函数的参数比原函数多或相等则执行原函数
if(args.length >= func.length){
return func(...args)
}else{
//否则一直累加新函数的参数
return function(...next){
return innerFunc(...args,...next)
}
}
}
}
或者
//答案2
function curry(func) {
return function innerFunc(...args) {
if(args.length >= func.length){
return func(...args)
//return func.apply(this,args)
}else{
//这里的bind作用是参数继承而不是改变this(我们应该清楚function内this都为window)
return innerFunc.bind(this,...args)
}
}
}
或者
//答案3 2022.1.21补充
function curry(fn) {
const l = fn.length;
let _curry = function (...rest) {
while (rest.length < l) {
return _curry.bind(this,...rest);
}
return fn.apply(this,rest);
}
return _curry;
}
无论怎样写,核心思想都离不开通过判断参数数量来决定返回结果还是继续收集参数。
补充
可能有的同学会遇到更过分的需求,让你的curry支持占位符。
const join = (a, b, c) => {
return `${a}_${b}_${c}`
}
const curriedJoin = curry(join)
const _ = curry.placeholder
curriedJoin(1, 2, 3) // '1_2_3'
curriedJoin(_, 2)(1, 3) // '1_2_3'
curriedJoin(_, _, _)(1)(_, 3)(2) // '1_2_3'
soEasy,让我们来实现它👇
function curry(fn) {
return function innerFunc(...args){
//如果参数都不是placeholder且长度满足,执行fn
const complete=args.length >= fn.length && !args.slice(0,fn.length).includes(curry.placeholder);
if(complete){
return fn.apply(this,args);
}else{
return function(...next){
//依次替换上一个函数的placeholder
const res=args.map(arg => arg === curry.placeholder && next.length ? next.shift() : arg);
return innerFunc(...res, ...next);
}
}
}
}
curry.placeholder = Symbol()
再补充
我觉得执行curry函数后,我们理应可以通过.length来判断剩余的参数数量,比如下面这样
const join = (a, b, c) => {
return `${a}_${b}_${c}`
}
const curriedJoin = curry(join)
curriedJoin(1, 2, 3).length // 0
curriedJoin(1).length // 2
curriedJoin(1, 2).length // 1
但是事实却是无论是答案1还是答案2,3都恒定返回0。
至于原因是什么,让我们先来了解一下Function.length的机制:
//length 是函数对象的一个属性值,指该函数有多少个必须要传入的参数,即形参的个数。
console.log(Function.length); //1
console.log((function() {}).length); //0
console.log((function(a) {}).length); //1
console.log((function(a, b) {}).length); //2
//形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。
console.log((function(...args) {}).length);
// 0,
//剩余参数(...args)为0
console.log((function(a, b = 1, c) {}).length);
// 1
//b=1声明了默认值,第一个具有默认值之前的参数个数为1
看到这里,我想大家都明白了:
无论是答案几,我们返回的innerFunc或者_curry都是使用的…args来收集剩余参数,因此自然无论是哪个阶段获取length都为0。
可能有同学会误认为上述原因有可能是bind方法导致的,看下面例子
const join = (a, b, c) => {
return `${a}_${b}_${c}`
}
console.log(join.bind(this,1).length) //2
function join2(...args){
return `${args[0]}_${args[1]}_${args[2]}`
}
console.log(join2.bind(this,1).length) //0
//可以看到,bind是会维持Function的length的,上面的两个函数区别就在于bind的原函数,如果原函数使用了(...args)来收集参数,那么使用bind后length也为0
最后,怎么让curry能够通过.length查看剩余参数呢
function curry(func) {
return function innerFunc(...args) {
if(args.length >= func.length){
return func(...args)
}else{
var boundLength = func.length-args.length
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
boundArgs.push('$' + i);
}
var binder= function(...next){
return innerFunc(...args,...next)
}
//Function 通过收集参数来创建一个新的函数。这这里通过该方法来动态传入boundArgs.length个参数。
//bound = function ($1,$2,...,$x){return binder.apply(this, arguments);}
var bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder);
return bound
}
}
}
重点解释一下代码结尾bound部分:
bound用立即执行函数直接换算过来为:
((binder) =>
//我们需要在该函数里放入boundArgs.length个参数,但是显然这种写法无法放入。
function ( 'boundArgs.join(',')' ) {
return binder.apply(this, arguments);
})(binder);