mqyqingfeng/Blog

JavaScript专题之函数柯里化

mqyqingfeng opened this issue · 141 comments

定义

维基百科中对柯里化 (Currying) 的定义为:

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

翻译成中文:

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

举个例子:

function add(a, b) {
    return a + b;
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3

用途

我们会讲到如何写出这个 curry 函数,并且会将这个 curry 函数写的很强大,但是在编写之前,我们需要知道柯里化到底有什么用?

举个例子:

// 示意而已
function ajax(type, url, data) {
    var xhr = new XMLHttpRequest();
    xhr.open(type, url, true);
    xhr.send(data);
}

// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")

// 利用 curry
var ajaxCurry = curry(ajax);

// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");

// 以 POST 类型请求来自于 www.test.com 的数据
var postFromTest = post('www.test.com');
postFromTest("name=kevin");

想想 jQuery 虽然有 $.ajax 这样通用的方法,但是也有 $.get 和 $.post 的语法糖。(当然 jQuery 底层是否是这样做的,我就没有研究了)。

curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。

可是即便如此,是不是依然感觉没什么用呢?

如果我们仅仅是把参数一个一个传进去,意义可能不大,但是如果我们是把柯里化后的函数传给其他函数比如 map 呢?

举个例子:

比如我们有这样一段数据:

var person = [{name: 'kevin'}, {name: 'daisy'}]

如果我们要获取所有的 name 值,我们可以这样做:

var name = person.map(function (item) {
    return item.name;
})

不过如果我们有 curry 函数:

var prop = curry(function (key, obj) {
    return obj[key]
});

var name = person.map(prop('name'))

我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些?

但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了?

person.map(prop('name')) 就好像直白的告诉你:person 对象遍历(map)获取(prop) name 属性。

是不是感觉有点意思了呢?

第一版

未来我们会接触到更多有关柯里化的应用,不过那是未来的事情了,现在我们该编写这个 curry 函数了。

一个经常会看到的 curry 函数的实现为:

// 第一版
var curry = function (fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    };
};

我们可以这样使用:

function add(a, b) {
    return a + b;
}

var addCurry = curry(add, 1, 2);
addCurry() // 3
//或者
var addCurry = curry(add, 1);
addCurry(2) // 3
//或者
var addCurry = curry(add);
addCurry(1, 2) // 3

已经有柯里化的感觉了,但是还没有达到要求,不过我们可以把这个函数用作辅助函数,帮助我们写真正的 curry 函数。

第二版

// 第二版
function sub_curry(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat([].slice.call(arguments)));
    };
}

function curry(fn, length) {

    length = length || fn.length;

    var slice = Array.prototype.slice;

    return function() {
        if (arguments.length < length) {
            var combined = [fn].concat(slice.call(arguments));
            return curry(sub_curry.apply(this, combined), length - arguments.length);
        } else {
            return fn.apply(this, arguments);
        }
    };
}

我们验证下这个函数:

var fn = curry(function(a, b, c) {
    return [a, b, c];
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

效果已经达到我们的预期,然而这个 curry 函数的实现好难理解呐……

为了让大家更好的理解这个 curry 函数,我给大家写个极简版的代码:

function sub_curry(fn){
    return function(){
        return fn()
    }
}

function curry(fn, length){
    length = length || 4;
    return function(){
        if (length > 1) {
            return curry(sub_curry(fn), --length)
        }
        else {
            return fn()
        }
    }
}

var fn0 = function(){
    console.log(1)
}

var fn1 = curry(fn0)

fn1()()()() // 1

大家先从理解这个 curry 函数开始。

当执行 fn1() 时,函数返回:

curry(sub_curry(fn0))
// 相当于
curry(function(){
    return fn0()
})

当执行 fn1()() 时,函数返回:

curry(sub_curry(function(){
    return fn0()
}))
// 相当于
curry(function(){
    return (function(){
        return fn0()
    })()
})
// 相当于
curry(function(){
    return fn0()
})

当执行 fn1()()() 时,函数返回:

// 跟 fn1()() 的分析过程一样
curry(function(){
    return fn0()
})

当执行 fn1()()()() 时,因为此时 length > 2 为 false,所以执行 fn():

fn()
// 相当于
(function(){
    return fn0()
})()
// 相当于
fn0()
// 执行 fn0 函数,打印 1

再回到真正的 curry 函数,我们以下面的例子为例:

var fn0 = function(a, b, c, d) {
    return [a, b, c, d];
}

var fn1 = curry(fn0);

fn1("a", "b")("c")("d")

当执行 fn1("a", "b") 时:

fn1("a", "b")
// 相当于
curry(fn0)("a", "b")
// 相当于
curry(sub_curry(fn0, "a", "b"))
// 相当于
// 注意 ... 只是一个示意,表示该函数执行时传入的参数会作为 fn0 后面的参数传入
curry(function(...){
    return fn0("a", "b", ...)
})

当执行 fn1("a", "b")("c") 时,函数返回:

curry(sub_curry(function(...){
    return fn0("a", "b", ...)
}), "c")
// 相当于
curry(function(...){
    return (function(...) {return fn0("a", "b", ...)})("c")
})
// 相当于
curry(function(...){
     return fn0("a", "b", "c", ...)
})

当执行 fn1("a", "b")("c")("d") 时,此时 arguments.length < length 为 false ,执行 fn(arguments),相当于:

(function(...){
    return fn0("a", "b", "c", ...)
})("d")
// 相当于
fn0("a", "b", "c", "d")

函数执行结束。

所以,其实整段代码又很好理解:

sub_curry 的作用就是用函数包裹原函数,然后给原函数传入之前的参数,当执行 fn0(...)(...) 的时候,执行包裹函数,返回原函数,然后再调用 sub_curry 再包裹原函数,然后将新的参数混合旧的参数再传入原函数,直到函数参数的数目达到要求为止。

如果要明白 curry 函数的运行原理,大家还是要动手写一遍,尝试着分析执行步骤。

更易懂的实现

当然了,如果你觉得还是无法理解,你可以选择下面这种实现方式,可以实现同样的效果:

function curry(fn, args) {
    var length = fn.length;

    args = args || [];

    return function() {

        var _args = args.slice(0),

            arg, i;

        for (i = 0; i < arguments.length; i++) {

            arg = arguments[i];

            _args.push(arg);

        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}


var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

或许大家觉得这种方式更好理解,又能实现一样的效果,为什么不直接就讲这种呢?

因为想给大家介绍各种实现的方法嘛,不能因为难以理解就不给大家介绍呐~

第三版

curry 函数写到这里其实已经很完善了,但是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,如果我不想根据这个顺序传呢?

我们可以创建一个占位符,比如这样:

var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", _, "c")("b") // ["a", "b", "c"]

我们直接看第三版的代码:

// 第三版
function curry(fn, args, holes) {
    length = fn.length;

    args = args || [];

    holes = holes || [];

    return function() {

        var _args = args.slice(0),
            _holes = holes.slice(0),
            argsLen = args.length,
            holesLen = holes.length,
            arg, i, index = 0;

        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            // 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标
            if (arg === _ && holesLen) {
                index++
                if (index > holesLen) {
                    _args.push(arg);
                    _holes.push(argsLen - 1 + index - holesLen)
                }
            }
            // 处理类似 fn(1)(_) 这种情况
            else if (arg === _) {
                _args.push(arg);
                _holes.push(argsLen + i);
            }
            // 处理类似 fn(_, 2)(1) 这种情况
            else if (holesLen) {
                // fn(_, 2)(_, 3)
                if (index >= holesLen) {
                    _args.push(arg);
                }
                // fn(_, 2)(1) 用参数 1 替换占位符
                else {
                    _args.splice(_holes[index], 1, arg);
                    _holes.splice(index, 1)
                }
            }
            else {
                _args.push(arg);
            }

        }
        if (_holes.length || _args.length < length) {
            return curry.call(this, fn, _args, _holes);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}

var _ = {};

var fn = curry(function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
});

// 验证 输出全部都是 [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)

写在最后

至此,我们已经实现了一个强大的 curry 函数,可是这个 curry 函数符合柯里化的定义吗?柯里化可是将一个多参数的函数转换成多个单参数的函数,但是现在我们不仅可以传入一个参数,还可以一次传入两个参数,甚至更多参数……这看起来更像一个柯里化 (curry) 和偏函数 (partial application) 的综合应用,可是什么又是偏函数呢?下篇文章会讲到。

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

日常学习

好难理解啊☹️

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)
jawil commented

专题系列专不知不觉更新了这么多,我得好好抽个时间系统学习一下😂

@jawil 哈哈~ 还有五篇就要完结了,可以等完结后一起看~

没有说道 什么场景下使用 柯里化

var name = person.map(function (item) {
    return item.name;
})
const fn = function (item) {
    return item.name;
}
person.map(fn)

PS:这样也可以参数复用

@yangchongduo 柯里化的应用更多在函数式编程中,可以看 JS 函数式编程指南,在 《JavaScript专题之函数组合》这篇文章中也有涉及部分。

@yangchongduo 这样确实可以做到参数复用,其实跟

var name = person.map(prop('name'))

是一样的,不过如果我们要获得其他属性呢?比如 'age'、'friends' 等,如果使用 fn 的话,还需要写多个 fn,而如果使用 prop 函数,就可以直接使用 prop('age') prop('friend')

高颜值写法的 judge = (...args) 用的好巧妙

function curry1(fn, args) {
var length = fn.length;
args = args || [];
return function (...arguments) {
args = [...args, ...arguments]
return args.length < length ? curry1.call(this, fn, args) : fn.apply(this, args);
}
}

值得一看~

深受启发,这样理解柯里化 :用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数,有没有毛病

@hlmjack 正是如此~ o( ̄▽ ̄)d

在更易懂的写法里return curry.call(this, fn, _args);,为什么要用this呢,感觉用null是不是也可以呢

@fi3ework 之所以写成 this 是因为希望根据环境的不同而设置不同的 this 值,我写了一个示例:

function curry(fn, args) {
    var length = fn.length;

    args = args || [];

    return function() {

        var _args = args.slice(0),

            arg, i;

        for (i = 0; i < arguments.length; i++) {

            arg = arguments[i];

            _args.push(arg);

        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}


var fn = curry(function(a, b, c) {
    console.log(this)
});

var obj = {
    value: 1,
    fn: fn
}

obj.fn(1, 2, 3);

这个时候的 this 的值为 obj 对象,如果设置成 null ,就会变成打印 window 对象

终于理解这句话了,“用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数”

我有个问题想请教下。
function (a, b, c){} 的参数有三个,fn('a', 'b', 'c', 'd') 正常工作,但 fn('a', 'b', 'c', 'd')('a') 就出错了。我知道这是因为 function (a, b, c){} 的参数个数,而导致的错误。
如果我想 fn('a', 'b',...n个参数)('a') 这样该如何写一个curry函数呢。

@bepromising fn('a', 'b', 'c', 'd')('a') 是因为 fn('a', 'b', 'c', 'd') 就已经执行了该函数,该函数如果没有再返回一个函数,就肯定报错,说这并不是一个函数。

至于 fn('a', 'b',...n个参数)('a'),我不清楚为什么还要再传个 'a' 呢?

柯里化 是真的绕啊

@BugHshine 是这篇写得不好,日后修订时会重写

第三版的代码是不是有bug呢,我试了一个稍微长一点的例子:

var _ = {}
var ary = curry(function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z) {
  return [a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z]
})
console.log(ary(1,_,_)(_)(5,6,7,8,9,10)(11,12,13,14)(15,_,_,18,_)(20,21,22,_)(24,25,26)(2,3,4)(16)(17,19)(23))

结果输出是不对的,node下:

[Function]

chrome下:

ƒ () { 
              var _args = args.slice(0),
                  _holes = holes.slice(0),
                  argsLen = args.length,
                  holesLen = holes.length,
                  a…

顺便我自己用es6写了一个稍微简洁一点的,对应原文第三版,可能参数多的情况下性能会有问题,但感觉一般情况下更好理解一些

// hole为自己的指定占位符
function curry(fn, hole) {
  const __len = fn.length
  let args = [],
  return function h() {
    // 先把参数放入args数组
    args = [...args, ...Array.from(arguments)]
    // 如果长度超过原有函数参数列表长度,表示有占位
    let holeNum = args.length - __len
    // 第一个占位符对应的肯定是__len位置的变量,循环将所有占位符替换
    for (let i = 0; i < holeNum; i++) {
      args[args.indexOf(hole)] = args[__len]
      args.splice(__len, 1)
    }
    // 如果没有占位符且参数数目已经够了
    if (args.length < __len || args.indexOf(hole) > -1) {
      return h
    } else {
      return fn.apply(null, args)
    }
  }
}

经测试上面的例子可以输出正确的结果

@izuomeng 这个是最终的版本吗?我这边有报错

<!DOCTYPE html>
<html>

<head>
    <title></title>
</head>

<body>
    <script>
    // hole为自己的指定占位符
    function curry(fn, hole) {
        const __len = fn.length
        let args = [];
        return function h() {
            // 先把参数放入args数组
            args = [...args, ...Array.from(arguments)]
            // 如果长度超过原有函数参数列表长度,表示有占位
            let holeNum = args.length - __len
            // 第一个占位符对应的肯定是__len位置的变量,循环将所有占位符替换
            for (let i = 0; i < holeNum; i++) {
                args[args.indexOf(hole)] = args[__len]
                args.splice(__len, 1)
            }
            // 如果没有占位符且参数数目已经够了
            if (args.length < __len || args.indexOf(hole) > -1) {
                return h
            } else {
                return fn.apply(null, args)
            }
        }
    }

    var _ = {};

    var fn = curry(function(a, b, c, d, e) {
        console.log([a, b, c, d, e]);
    });

    fn(1, 2, 3, 4, 5);
    fn(_, 2, 3, 4, 5)(1);
    fn(1, _, 3, 4, 5)(2);
    fn(1, _, 3)(_, 4)(2)(5);
    fn(1, _, _, 4)(_, 3)(2)(5);
    fn(_, 2)(_, _, 4)(1)(3)(5)
    </script>
</body>

</html>

default

少了一行,抱歉😄

// hole为传入的占位符
function curry(fn, hole) {
  const __len = fn.length
  let args = [];
  return function h() {
      // 先把参数放入args数组
      args = [...args, ...Array.from(arguments)]
      // 如果长度超过原有函数参数列表长度,表示有占位
      let holeNum = args.length - __len
      // 第一个占位符对应的肯定是__len位置的变量,循环将所有占位符替换
      for (let i = 0; i < holeNum; i++) {
          args[args.indexOf(hole)] = args[__len]
          args.splice(__len, 1)
      }
      // 如果没有占位符且参数数目已经够了
      if (args.length < __len || args.indexOf(hole) > -1) {
          return h
      } else {
          fn.apply(null, args)
          return args = []
      }
  }
}

@izuomeng 这个占位的 curry 比你想的要复杂一点…… 因为对于 fn(1, _, 3)(_, 4)(2)(5); 这样的例子,结果应该是 [1, 2, 3, 4, 5],如果使用这种方法的话,结果会是 [1, 2, 3, 5, 4]

哦哦,看了几组执行结果才发现这个占位是边执行边填补的,我把他理解成从超出参数列表列表长度之后才开始填补的了,谢谢指教🙏

@izuomeng 但是你说的问题还是存在的,而且现在的写法也很繁琐,我也在想更加简洁易懂的写法~

Pa-wN commented

请教下博住,第三版的填占位符是怎样一个规律。。。。

我现在才知道 function 的 length 竟然就是参数的长度,太水了 😄

很喜欢你的第二版,逻辑很清晰,很赞

@iiicon 这算是个很细的知识啦,我也是很晚才知道的~

var curry = (fn, ...args) =>
        fn.length <= args.length
            ? fn(...args)
            : curry.bind(null, fn, ...args)
var fn = curry(function(a, b, c){
  console.log([a,b,c])
})
fn(1,2,3)
fn(1,2)(3)
fn(1)(2,3)
fn(1)(2)(3)
[ 1, 2, 3 ]
[ 1, 2, 3 ]
[ 1, 2, 3 ]
[ 1, 2, 3 ]

参考_30s.js

@Wiolem 好方法,感谢补充~ ☆´∀`☆

对于博主第二版的极简版的解释有点不太理解。
当执行 fn1()() 时,函数返回:

curry(sub_curry(function(){
   return fn0()
}))
// 相当于
curry(function(){
  return (function(){
    return fn0()
  })()
 })

那么

(function(){
   return fn0()
})()

这个函数立即执行了,那么fn0函数也就执行了。此时工作台就因该有‘1’出现了啊。


下面是我对于博主第二版的极简版的理解:如有错误还请指出。

var fn = curry(fn0)

当执行 fn() 时,函数返回:

curry(sub_curry(fn0))
// 相当于
curry(function(){
    return fn0();
})

此时我只需要把

(function(){
    return fn0();
}

看做传入函数(起个名字为fn1吧)

curry(fn1);
//对比curry(fn0),似是故人来。

当执行 fn()() 时,函数返回:

curry(sub_curry(fn1))
// 相当于
curry(function(){
    return fn1();
})

同样可以看做传入函数(起个名字为fn2)。

curry(fn2);

当执行 fn()()() 时,同样

curry(function(){
    return fn2();
})

同样可以看做传入函数(起个名字为fn3)。

curry(fn3);

当执行 fn()()()() 时,此时已不满足判断条件 if (length > 1) ,于是

return fn();

此时的传入的参数fn为fn3,

fn3()  就是 :
(function(){
    return fn2();
})();

fn2 =function(){
    return fn1();
};

fn1 =function(){
    return fn0();
};
此时工作台才会出现‘1’

@flymie 不会执行呀

curry(sub_curry(function(){
   return fn0()
}))
// 相当于
curry(function(){
  return (function(){
    return fn0()
  })()
 })
// 相当于
curry(function(){
    return fn0()
})

这时候不会执行呀~

@mqyqingfeng
可能是我的描述有问题。
感觉sub_curry(fn)应该加了一条输出比较好。

function sub_curry(fn){
    return function(){
        console.log(1);  //加了这条输出语句
        return fn()
    }
}

那么执行 fn1()()时候,

curry(sub_curry(function(){
   console.log(1);
   return fn0()
}))
// 相当于
curry(function(){
  return (function(){
    console.log(1);
    return fn0()
  })()
 })
// 相当于
curry(function(){
    return fnx();   //应该是相当于fnx吧。原句的这里是fn0,感觉就像函数执行过一样。
})

@flymie sub_curry返回的这个函数,在参数不全(此处是调用次数不足)的情况是肯定不会触发,因为sub_curry永远都是返回一个新的函数,而不会去执行那个函数。来完整还原一下流程(去除了if条件):
首先是fn1

curry(fn0)
// 相当于
function(){
  // length = 4 
  return curry(sub_curry(fn0), --length)
}

然后是fn1()

(function() {
  return curry(sub_curry(fn0), --length);
})()
// 相当于
curry(function(){
  return fn0()
}, 3)
// 相当于
function() {
  // length = 3
  return curry(sub_curry(function(){
    return fn0();
  }), --length)
}

最后就是fn1()()了

(function() {
  return curry(sub_curry(function(){
    return fn0();
  }), --length)
})()
// 相当于
curry(function() {
  return (function(){
    return fn0();
  })()
}), 2)
// 相当于
function() {
  // length = 2
  return curry(sub_curry(function() {
    return (function(){
      return fn0();
    })()
  }), --length)
}

也就是说:只要你的调用次数没有到达规定限度,随着调用次数的增加,传给curry这个方法的函数层级会越来越深。直到调用次数够了,才会一口气将这个函数执行完,你想要的console.log(1)也才会触发

函数的length属性获取形参的个数,但是形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数。

((a, b, c = 3) => {}).length; // 2
((a, b = 2, c) => {}).length; // 1
((a = 1, b, c) => {}).length; // 0
((...args) => {}).length; // 0

如果使用了ES6的函数参数默认值,可能不适用柯里化的场景。

const fn = curry((a = 1, b, c) => {
  console.log([a, b, c]);
});
fn()(2)(3); // Uncaught TypeError: fn(...) is not a function

在这种情况下,期望输出的是[1, 2, 3],而实际上fn()已经输出了[1, undefined, undefined],而不是返回闭包函数。所以fn()(2)将会报错。

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
   judge = (...args) =>
       args.length === fn.length
           ? fn(...args)
           : (arg) => judge(...args, arg)

参数不满足长度要求时,返回的函数仍需使用rest parameter:(...arg) => judge(...args, ...arg),否则除了第一次可以传递多个参数,后面只能一个一个传到足够为止了,不能直接处理fn(1,2)(3,4,5)的情况。

function curry(length, fn) {
	if (typeof length === 'function')
		return curry.apply(this, Array.prototype.concat.apply([length.length], arguments));

	var _args = arguments;
	return function () {
		var args = Array.prototype.slice.call(_args);
		var i, next = 0, placeholderp = false;

		while (++next < args.length && args[next] != curry._);
		for (i = 0; i < arguments.length; i++) {
			args[next] = arguments[i];
			placeholderp |= args[next] === curry._;
			while (++next < args.length && args[next] != curry._);
		}

		if (placeholderp || next < 2 + length || next < args.length)
			return curry.apply(this, args);
		else
			return fn.apply(this, args.slice(2));
	};
}
curry._ = {};




var _ = curry._;

var fn = curry(function (a, b, c, d, e) {
	console.log([a, b, c, d, e]);
}, _, _, 3, 4, _);

fn(1, 2)(5);
fn(1, _)(2, _)(5);
// [1, 2, 3, 4, 5]

var arity3 = curry(3, function () {
	console.log(arguments);
});
var arity2 = arity3(1);
var arity1 = arity3(1, 5) || arity2(5);
arity2(1, 2);
// [1, 1, 2];
arity1(2);
// [1, 5, 2]

var square = curry(Math.pow)(_, 2);
square(3);
// 9

var cubic = curry(Math.pow, _, 3);
cubic(2);
// 8

好文,可惜现在内力不足,还不是很能消化

我现在才知道 function 的 length 竟然就是参数的长度,太水了

很喜欢你的第二版,逻辑很清晰,很赞

其实表示的是未设置默认值的参数数量
比如

function test (arg1, arg2, arg3 = 3 ){
}

这里test.length=2

SunLn commented

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

最后 (arg) => judge(...args, arg) , 修改为 (...arg) => judge(...args, ...arg) ,才进一步完善。

不然 fn("a")("b", "c")('d') 这种情况就会只接收了 a, b, d 三个参数而出现问题

rest可以这样用麽,记忆中是must be last

版本二写成这样不知可不可行?

function add(a, b, c, d) {
  return a + b + c + d;
}

function curry(fn, length) {
  return function (...args) {
    if (args.length < length) {
      return curry(fn.bind(this, ...args), length - args.length )
    } else {
      return fn.apply(this, args)
    }
  }
}

var addCurry = curry(add, 4);
console.log(addCurry(1)(2, 3, 4))
console.log(addCurry(1, 2, 3, 4)) // 10

@xiaohesong

rest 参数之后不能再有其他参数。

// 报错
function f(a, ...b, c) {
  // ...
}

// 正确
function f(a, b, ...c) {
  // ...
}

但调用函数时,... 是数组的扩展运算符 (spread) 。例如 fn(...[1, 2, 3, 4]) ,它用来把一个数组转换为用逗号分隔的参数序列。
扩展运算符与正常的函数参数可以结合使用,例如 fn(0, ...[1, 2, 3, 4], 5)

@inottn 嗯是的,看错了。

我认为@大笑平老师的写法是非常赞的。
我在这里针对这个方法做了一个小的优化,
用于解决:curry4(1, 2)(3, 4)

const curry = fn =>
   judge = (...args) => 
      args.length === fn.length
         ? fn(...args)
         : (...arg) => judge(...args, ...arg)

柯里化的同时可以传入参数
const currying = (fn, ...arg1) => (...arg2) => [...arg1, ...arg2].length >= fn.length ? fn.apply(null, [...arg1, ...arg2]) : currying(fn, ...arg1, ...arg2)

我认为@大笑平老师的写法是非常赞的。
我在这里针对这个方法做了一个小的优化,
用于解决:curry4(1, 2)(3, 4)

const curry = fn =>
   judge = (...args) => 
      args.length === fn.length
         ? fn(...args)
         : (...arg) => judge(...args, ...arg)

这么写就不是柯里化了,柯里化一次传一个参数

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

实践了一下,这里最后一行的参数只能在接受单个。应该写成:

let curry = fn =>
  judge = (...args) =>
    args.length === fn.length
      ? fn(...args)
      : (...argMore) => judge(...args, ...argMore);

这样测试fn('a')('b','c')时才能跟之前一样

大大佬 你好,第三版的科里化,有个缺陷, 这种执行 fn(1, _, _, 4)(2, 3, 5) 就不能返回结果

//函数柯理化  其本质是降低通用性,提高适用性,其实是对闭包的极致应用
//原理:利用闭包保存一部分参数,返回一个包含了一部分参数的闭包。
//适用场景: ...

function connect(a, b, c, d,e,f,g) {
    console.log(`${a}-${b}-${c}-${d}-${e}-${f}-${g}`);
}

//在闭包中,A闭包 由 B函数生成
//使用闭包可以引用函数的变量对象这一性质
//把闭包中存的变量和闭包接受的实参组合起来,传入目标函数
//简易版
function simpleCurry(fn){
    let args = [].slice.call(arguments, 1);
    return function () {
        fn.apply(this, args.concat([].slice.call(arguments)))
    }
}

//只可以分成两步,如果要可以分成任意层,就得用递归了
// simpleCurry(connect, 1, 2,5,67,8,4)(3);
// simpleCurry(connect, 1)(2, 3,4,5,6, 28);

//完整版,接受N层闭包,由于层数不定,递归也必须用到
//递归的过程中不断的 聚集参数,直到参数达到目标函数需要的个数,就执行函数
//如何知道函数接受的理想参数个数  fn.length
function curry(fn, args){
    let length = fn.length; //目标函数理想的参数个数
    let allArgs = args || [];
    return function () {
        let _args = [].slice.apply(arguments);
        let _allArgs = allArgs.concat(_args)
        //未达到理想参数个数就继续聚集参数, 达到了参数的个数,就可以运行目标函数
        if (_allArgs.length < length){
            //聚集参数的过程
            return curry.call(this, fn, _allArgs)
        }
        else{
            fn.apply(this, _allArgs);
        }
    }
}

curry(connect)(2, 3, 4, 5)(6, 1)(2);

//如果不想按顺序传入,则可以先用占位符,后面再填入数据
//比如
/**
 * 
let fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", _, "c")("b") // ["a", "b", "c"]
 * 
 * 
 */
let _;
function curry2(fn, args){
    let allArgs = args || [];
    let length = fn.length;
    return function () {
        let _args = [].slice.call(arguments);
        let _allArgs = [].slice.call(allArgs);
        //在这里来调整参数的位置, 如果前面有占位符就向前补位

        if (_args.indexOf(_) !== -1){
            //有占位符  就直接concat
            _allArgs = _allArgs.concat(_args);
        }
        else{
                //没有占位符,说明这段参数可以向前补位
            _allArgs.forEach((v, i) => {
                if (v === _ && _args.length != 0) {
                    _allArgs.splice(i, 1, _args.shift());
                }
            })
            //剩下的还是添加进参数里面
            if (_args.length != 0){
                _allArgs = _allArgs.concat(_args);
            }
        }
            //是否达到理想参数个数  以及是否还含有空位
        if (_allArgs.length < length || _allArgs.indexOf(_) !== -1){
            //继续聚集参数
            return curry2.call(this, fn, _allArgs);
        }
        else{
            fn.apply(this, _allArgs);
        }
    }
}

curry2(connect)(2, _, 4, 5)(_, 1)(_)("占位1", "占位2")("占位3");

最后一种参数不定的,自己用自己的想法写了一种。本文作者讲得比较难理解的那种,看了半天,真的难以理解。

大大佬 你好,第三版的科里化,有个缺陷, 这种执行 fn(1, _, _, 4)(2, 3, 5) 就不能返回结果

第三版存在的问题是无法同时传不连续的两个参,如:fn(1, _, , 4)(, 3)(2, 5)
问题是出在else if (holesLen) 这里, fn执行到(2,5)这一步时,_holes是 [1],当arguments循环到5时,仍然替换2时候的位置。只要添加个holesLen-- 就可以了。
else {
holesLen--
_args.splice(_holes[index], 1, arg);
_holes.splice(index, 1)
}

curry化要求被传入函数所有参数都被明确的定义,因此当使用部分参数调用时,他会返回一个新的函数,在真正调用之前等待外部提供其余的参数。可以简单的理解为,在所有参数被提供之前,挂起或延迟函数的执行
--------------------------------以上是来自《javascript函数式编程指南》一书.
希望能帮助大家理解。

大佬。 柯里化 return curry.call(this, fn, newArgs) 这个this本来就是指向的window, 为什么不直接 return curry(fn, newArgs)。

看了好几遍,,一步步的console ,,终于懂了,,感谢大佬

占位符,那个地方,我看蒙住了T_T。
对于这个 fn(_, 2)(_, _, 4)(1)(3)(5)
想象中我以为等价为 fn(1, 2, 3, 5, 4)
可结果是 fn(1, 2, 3, 4, 5)
我是这样想的,先让占位符和非占位参数长度达到fn.length,之后再传进来的非占位参数依次替换掉之前的占位符。我这样想是不是错了,小白很慌,求救T_T
@mqyqingfeng

至此,我们已经实现了一个强大的 curry 函数,可是这个 curry 函数符合柯里化的定义吗?柯里化可是将一个多参数的函数转换成多个单参数的函数,但是现在我们不仅可以传入一个参数,还可以一次传入两个参数,甚至更多参数……这看起来更像一个柯里化 (curry) 和偏函数 (partial application) 的综合应用,可是什么又是偏函数呢?下篇文章会讲到。

那么符合函数柯里化的定义啊吗???

试着写了个

function hasEmptyCurry (fn, hole = '_', ...args) {
        // 存储被柯里化函数的形参个数
        var beCurryFnLen = fn.length;
        return function (...curryArgs) {
          var j = 0,
          combinationArgs = [],
          holes = false;

          for (var i = 0, len = args.length; i < len; i++) {
            if (args[i] === hole) {
              if (curryArgs[j] === undefined || curryArgs[j] === hole) {
                ++j;
              } else {
                args[i] = curryArgs[j];
                curryArgs.splice(j, 1);
              }
            }
          }
          
          combinationArgs = args.concat(curryArgs);
          holes = combinationArgs.indexOf(hole) !== -1;
          
          if (holes || combinationArgs.length < beCurryFnLen) {
            // 如果实参个数小于形参个数,则继续柯里化
            return hasEmptyCurry.call(this, fn, hole, ...combinationArgs);
          } else {
            // 如果实参个数等于形参个数,则直接返回fn的执行结果
            return fn.apply(this, combinationArgs);
          }
        }
      }

很久之前写过一个函数柯里化 无限层级 参数个数任意

var currying = function (fu) { 
    var _arg = [];
    return function cb() {
        if (arguments.length === 0) {

            return fu.apply(null, _arg)
        }
        Array.prototype.push.apply(_arg, [].slice.call(arguments))
        return cb
    }
}
var sum = function () {

    var total = 0;
    var argArray = Array.prototype.slice.call(arguments)
    argArray.forEach(function (item) {
        total += item
    });
    return total
}
var toUpperCase = function () {
    var total = []
    var argArray = Array.prototype.slice.call(arguments)
    argArray.forEach(function (item) {
        total.push(item.toUpperCase())
    });
    return total
}
var fun1 = currying(sum);
var fun2 = currying(toUpperCase);
fun1(1, 2, 3)(1, 2, 3)(1, 2, 3, 1, 2, 3)
fun2('a', 'a', 'a')('a', 'a', 'a')('a', 'a', 'a')('a', 'a', 'a')('a', 'a', 'a')('a', 'a', 'a')('a', 'a', 'a')('a', 'a', 'a')
console.log(fun1());
console.log(fun2());

内力不够,看的真吃力...

柯里化还是会丢this,仍然不能把crry得到的函数等同一个不curry的函数,下个回答
我附了一个修改版,解决了这个毛病。
function curry(fn, args) {
var length = fn.length;

args = args || [];

return function() {

    var _args = args.slice(0),

        arg, i;

    for (i = 0; i < arguments.length; i++) {

        arg = arguments[i];

        _args.push(arg);

    }
    if (_args.length < length) {
        return curry.call(this, fn, _args);
    }
    else {
        return fn.apply(this, _args);
    }
}

}

var fn = curry(function(a, b, c) {
console.log(this.value)
});

var obj = {
value: 1,
fn: fn
}

obj.fn(1,2)(3);
undefined

function curry(fn, args) {
var length = fn.length;
args = args || [];
var that = this;
return function() {

var _args = args.slice(0),

    arg, i;

if(that !== window)that_to = that;
else that_to = this;

for (i = 0; i < arguments.length; i++) {

    arg = arguments[i];

    _args.push(arg);

}
if (_args.length < length) {
    return curry.call(that_to, fn, _args);
}
else {
    return fn.apply(that_to, _args);
}

}
}
var fn = curry(function(a, b, c) {
console.log(this.value)
});
var obj = {
value: 1,
fn: fn
}
obj.fn(1,2)(3);
稍微改了一下,解决了丢this的毛病,不过这也只适用于非严格模式了

zjgyb commented

我试着写了一下,不知道对不对。

实现文中更易懂的实现方式

function curry(fn, args) {
  var length = fn.length;

  args = args || [];
  return function() {
    var _args = args.concat([].slice.call(arguments));
    if (_args.length < length) {
      return curry.call(this, fn, _args);
    }
    return fn.apply(this, _args);
  };
}

实现第三版

function curry(fn, args) {
  var length = fn.length;
  args = args || [];
  return function() {
    var newArgs = [].slice.call(arguments);
    for (var i = 0, len = args.length; i < len; i++) {
      if (args[i] === _) {
        args.splice(i, 1, newArgs.shift());
      }
      if (newArgs.length === 0) break;
    }
    var _args = args.concat(newArgs);
    var _filterArr = _args.filter(ele => ele !== _);
    if (_filterArr.length < length) {
      return curry.call(this, fn, _args);
    }
    return fn.apply(this, _args);
  };
}

简单点
`

function curry (func) {
  return function () {
    return arguments.length === func.length ? func(...arguments) : curry (func).bind(null, ...arguments)
  }
}

`

const add = (...arg) => arg.reduce((prev, curr) => curr += prev, 0);
const currying = (fn, ...arg1) => (...arg2) => ((arg2.length === 0) ? fn(...arg1) : currying.call(null, fn, ...arg1, ...arg2))
const curryAdd = currying(add);
curryAdd(1, 2)(3)(4)();
curryAdd(1, 2)(3, 4)();
curryAdd(1)(2)(3)(4)();

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

简单点
`

function curry (func) {
  return function () {
    return arguments.length === func.length ? func(...arguments) : curry (func).bind(null, ...arguments)
  }
}

`

参数有可能超过,不能等于

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

这种写法有一些问题吧?this的绑定问题以及judge变量的污染。

// example
var judge = 'judge'
var name = 'window'
var obj = {
  name: 'obj',
  say: function(a, b, c, d){ return `${this.name}'s params: ${[a, b, c, d].join(',')}` }
}
console.log(typeof judge) // => "string"
var curriedSay = curry.call(obj, obj.say) // 无法修改`this`的指向
console.log(curriedSay('a', 'b')('c')('d')) // => "window's params: a,b,c,d"
console.log(typeof judge) // => "function"

好难啊 感觉自己得多看几遍了

感觉像bind函数的实现

有点无法理解第二版里面为啥要传一个length,还有就是fn.length 代表什么

有点无法理解第二版里面为啥要传一个length,还有就是fn.length 代表什么

length || fn.length 前面传length是记录当前已经收到几个参数了(如果收够了就执行),后面 fn.length是第一层处理时记录下被柯里化的函数的参数总个数

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

果然高颜值,这里稍稍有些手误:

console.log(fn(1)(2, 3));   // arg => judge(...args, arg)

改下就好了:

var curry = fn =>
  (judge = (...args) =>
    args.length === fn.length 
    ? fn(...args) 
    : (...arg) => judge(...args, ...arg));

经历了这么多次面试这种定长 curry 问题我好像一次都没遇到过. 反倒是不定长 curry 的考察频频出现:

如实现这样一个 add: (头条一面, 腾讯一面考了同一题)

add(1); // 1
add(1)(2); // 3
add(1)(2)(3); // 5

这样的题目其实是有问题的, 因为确实没有办法预先获取这个调用链的长度. 面试官应该传达一点, 其实给的注释并不代表 return 值...

因此, 相应的实现有:

const add = sum => {
    const fn = n => add(n + sum);

    fn.valueOf = () => sum;

    return fn;
}

/** Test **/
add(1); // Function
+add(1); // 1
+add(1)(2); // 3
+add(1)(2)(3); // 5

按照犀牛书的定义, 这个可能不属于 curry , 就只是纯粹的链式调用罢了. 尽管有些混淆, 还是跟大家分享一下.

var combined = [fn].concat(slice.call(arguments));
这个slice是不是得写成[].slice,我这里报错

let _={};
function curry(fn, args) {
  var length = fn.length;

  args = args || [];

  return function() {

      var _args = args.slice(0),arg, i;
      _args.forEach((item,index,arr) =>{
        if(item==_&&arguments.length>0){
          arr[index]=[].shift.call(arguments)
        }
      });
      for (i = 0; i < arguments.length; i++) {
          arg = arguments[i];
          _args.push(arg);
      }
      if (_args.length < length||_args.includes(_)) {
          return curry.call(this, fn, _args);
      }
      else {
          return fn.apply(this, _args);
      }
  }
}
var fn = curry(function(a, b, c, d, e) {
  console.log([a, b, c, d, e]);
})
``` ```
这个最终版感觉比较简洁 @mqyqingfeng 

第二版这样写是不是更简洁:

function curry(fn) {
    let totalArgLength = fn.length;
    let totalArguments = [];
    const slice = Array.prototype.slice;
    return function () {
        totalArguments = [].concat(totalArguments, slice.call(arguments, 0))
        let argLength = arguments.length;
        if (argLength < totalArgLength) {
            totalArgLength -= argLength;
            return arguments.callee
        } else {
            return fn.apply(null, totalArguments)
        }
    }
}

bind就是柯里化的实现啊

function a(a, b){
    console.log(a, b);
}

var fn = a.bind(this, "a");
fn("b");

经历了这么多次面试这种定长 curry 问题我好像一次都没遇到过. 反倒是不定长 curry 的考察频频出现:

如实现这样一个 add: (头条一面, 腾讯一面考了同一题)

add(1); // 1
add(1)(2); // 3
add(1)(2)(3); // 5

这样的题目其实是有问题的, 因为确实没有办法预先获取这个调用链的长度. 面试官应该传达一点, 其实给的注释并不代表 return 值...

因此, 相应的实现有:

const add = sum => {
    const fn = n => add(n + sum);

    fn.valueOf = () => sum;

    return fn;
}

/** Test **/
add(1); // Function
+add(1); // 1
+add(1)(2); // 3
+add(1)(2)(3); // 5

按照犀牛书的定义, 这个可能不属于 curry , 就只是纯粹的链式调用罢了. 尽管有些混淆, 还是跟大家分享一下.

这里不定参数的实现好赞呀.. 但是这里调用的时候只能传入单个参数,针对这种add(1)(2, 3)(4, 5, 6)就没有办法啦。按照楼上的方法,实现了个多参数的add版本

const add = (...param: number[]): Function => {
    const addSum = param.reduce((prev, next) => prev + next)
    const fn = (...args: number[]) => {
        const sum = args.reduce((prev, next) => prev + next)
        return add(addSum + sum)
    }
    fn.valueOf = () => addSum
    return fn
}

console.log(+add(1)(1, 1, 1)(1)) // 5
console.log(+add(1)(2)(3, 4)) // 10

var combined = [fn].concat(slice.call(arguments));
这个slice是不是得写成[].slice,我这里报错

你看漏了一行
var slice = Array.prototype.slice;
这个slice 其实是这个

image
这个实现是有严重BUG的,举个例子:
const bugFn = (a, b, c = 1) => {};
柯里化这个函数就废了。此时bugFn的length是2,但是他是3个参数。所以不能够用这种方式柯里化有默认值的函数

这么说, subcurry 和 高颜值版的 : (arg) => judge(...args, arg);有异曲同工之妙

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

这有个问题.
例如:

const add = (x, y, z) => x + y + z;
const test = curry(add);
test(1)(2, 3)
// 返回的是函数不是6。因为判断只积累了两个参数。

应该改成这样吧:

const curry = (fn) =>
    judge = (...args) =>
        fn.length === args.length
            ? fn(...args)
            : (...nextArgs) => judge(...args, ...nextArgs);

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

这有个问题.
例如:

const add = (x, y, z) => x + y + z;
const test = curry(add);
test(1)(2, 3)
// 返回的是函数不是6。因为判断只积累了两个参数。

应该改成这样吧:

const curry = (fn) =>
    judge = (...args) =>
        fn.length === args.length
            ? fn(...args)
            : (...nextArgs) => judge(...args, ...nextArgs);

我觉得用函数的length属性去判断是不可取的。
比如const add = (x, y = 0, z = 0) => x+y+z;
就无法达到想要的效果。
又比如函数(...rest) => rest[0] + rest[1] + rest[2];
都无法用所说的实现方式去柯里化

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

这有个问题.
例如:

const add = (x, y, z) => x + y + z;
const test = curry(add);
test(1)(2, 3)
// 返回的是函数不是6。因为判断只积累了两个参数。

应该改成这样吧:

const curry = (fn) =>
    judge = (...args) =>
        fn.length === args.length
            ? fn(...args)
            : (...nextArgs) => judge(...args, ...nextArgs);

我觉得用函数的length属性去判断是不可取的。
比如const add = (x, y = 0, z = 0) => x+y+z;
就无法达到想要的效果。
又比如函数(...rest) => rest[0] + rest[1] + rest[2];
都无法用所说的实现方式去柯里化

确实有很多限制

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

这种写法有一些问题吧?this的绑定问题以及judge变量的污染。

// example
var judge = 'judge'
var name = 'window'
var obj = {
  name: 'obj',
  say: function(a, b, c, d){ return `${this.name}'s params: ${[a, b, c, d].join(',')}` }
}
console.log(typeof judge) // => "string"
var curriedSay = curry.call(obj, obj.say) // 无法修改`this`的指向
console.log(curriedSay('a', 'b')('c')('d')) // => "window's params: a,b,c,d"
console.log(typeof judge) // => "function"

改成这样就行。至于你说的污染问题,不存在的。因为你开始你就要在父作用域去定义这个变量。

let judge;
const curry = function(fn) {return judge = (...args) => fn.length === args.length ? fn.call(this, ...args) : (...nextArg) => judge(...args, ...nextArg)};

用ES6的写法,可以借助bind实现

function curry (fn, ...args) {
    return args.length >= fn.length ? fn(...args) : curry.bind(null, fn, ...args)
}

项目里自用的实现,几乎是纯函数式写的项目,所以不存在 this 的问题。

export const isObject = obj => Object.prototype.toString.call(obj) === '[object Object]'
export const hasOwnProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)

export const argPlaceholder = {
  // compatible with ramda.js
  '@@functional/placeholder': true,
  isArgPlaceholder: true
}
export const isArgPlaceholder = placeholder => isObject(placeholder) && hasOwnProperty(placeholder, 'isArgPlaceholder') && placeholder.isArgPlaceholder

export const simpleCurry = (fn, ...args) => args.length >= fn.length ? fn(...args) : (...args2) => curry(fn, ...args, ...args2)
export const internalCurry = (fn, filled, ...args) => {
  let innerArgs = filled || []
  innerArgs = innerArgs.map(innerArg => {
    return isArgPlaceholder(innerArg) ? (args.shift() || innerArg) : innerArg
  })
  innerArgs = innerArgs.concat(args)
  const innerLen = innerArgs.slice(0, fn.length).reduce((len, innerArg) => isArgPlaceholder(innerArg) ? len : len + 1, 0)
  if (innerLen >= fn.length) {
    return fn(...innerArgs)
  } else {
    return (...args2) => internalCurry(fn, innerArgs, ...args2)
  }
}
export const curry = (fn, ...args) => internalCurry(fn, [], ...args)

献丑,来个不定长的,面试快乐版。。其实我感觉这。。不柯里,面试看到这种题,基本确定面试官不懂柯里化。

function add2() {
  let argsSum = Array.from(arguments).reduce((prev, next) => prev + next)
  let fn = function () {
    return add2(Array.from(arguments).reduce((prev, next) => prev + next) + argsSum)
  }
  fn.valueOf = () => argsSum
  return fn
}
console.log(+add2(1, 2)(1))
jawil commented

搞个面试背诵版本

const curry = (fn, ...curryArgs) => (...args) =>
  args.length >= fn.length
    ? fn(...curryArgs, ...args)
    : curry(fn, ...curryArgs, ...args);

搞个面试背诵版本

const curry = (fn, ...curryArgs) => (...args) =>
  args.length >= fn.length
    ? fn(...curryArgs, ...args)
    : curry(fn, ...curryArgs, ...args);

大佬, 这里 args.length >= fn.length 长度判断 应该还要加上 curryArgs.length 起始参数的长度把。

const curry = (fn, ...curryArgs) => (...args) =>
  (args.length + curryArgs.length) >= fn.length
    ? fn(...curryArgs, ...args)
    : curry(fn, ...curryArgs, ...args);

function add(a, b, c, d) {
  console.log(a + b + c + d);
}

const _curry = curry(add, 1, 2, 3);

_curry(4);

第三版中好像有一个bug:

  • 如果占位符被消耗完了还继续添加参数的话只会添加到第一个位置而不会新增,原因是_args.splice(_holes[index], 1, arg);_holes[index]是undefined。测试函数:fn(1, _)(2, 3, 4, 5)

稍稍改动了下:

// 第三版
function curry(fn, args, holes) {
    length = fn.length;

    args = args || [];

    holes = holes || [];

    return function() {

        var _args = args.slice(0),
            _holes = holes.slice(0),
            argsLen = args.length,
            holesLen = holes.length,
            arg, i, index = 0;

        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            // 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标
            if (arg === _ && holesLen) {
                index++
                if (index > holesLen) {
                    _args.push(arg);
                    _holes.push(argsLen - 1 + index - holesLen)
                }
            }
            // 处理类似 fn(1)(_) 这种情况
            else if (arg === _) {
                _args.push(arg);
                _holes.push(argsLen + i);
            }
            // 处理类似 fn(_, 2)(1) 这种情况
            else if (holesLen) {
                // fn(_, 2)(_, 3)
                if (index >= holesLen) {
                    _args.push(arg);
                }
                // fn(_, 2)(1) 用参数 1 替换占位符
                else {
                   //  _args.splice(_holes[index], 1, arg);
                   //  _holes.splice(index, 1)
                  // 改动在这里
                  // 判断是否还存在占位符
                  if (_holes.length) {
                    _args.splice(_holes[index], 1, arg);
                    _holes.splice(index, 1)
                  } else {
                    _args.push(arg);
                  }
                }
            }
            else {
                _args.push(arg);
            }

        }
        if (_holes.length || _args.length < length) {
            return curry.call(this, fn, _args, _holes);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}

var _ = {};

var fn = curry(function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
});

fn(1, _)(2, 3, 4, 5); // 改动前数组长度永远都是2

占位符,那个地方,我看蒙住了T_T。
对于这个 fn(_, 2)(_, _, 4)(1)(3)(5)
想象中我以为等价为 fn(1, 2, 3, 5, 4)
可结果是 fn(1, 2, 3, 4, 5)
我是这样想的,先让占位符和非占位参数长度达到fn.length,之后再传进来的非占位参数依次替换掉之前的占位符。我这样想是不是错了,小白很慌,求救T_T
@mqyqingfeng

占位符是边传边填补的,也就是说fn(_, 2)(_, _, 4)(1)(3)(5)第二个函数里的占位符会填补第一个函数的占位符,我理解为占位符顺延。对于第二个函数的参数来说,第一个占位符表示填充第一个函数中的占位符,此时占位符填满了,第二个占位符会新增上去。然后参数1填充第一个占位符,参数3填充第二个占位符,此时占位符已经用完了,参数5添加到末尾。
数组分别是[_, 2][_, 2, _, 4][1, 2, _, 4][1, 2, 3, 4][1, 2, 3, 4, 5]

这里理解的关键是第二个占位符会顺延第一个占位符到下一个函数,而不是新增一个占位符。

segmentfault 的@大笑平 补充的高颜值写法:

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, arg)

这里依赖了fn.length
但是fn.length的取值逻辑可能是不与预期相符合的,会导致无效

const add = (a=1,b=1) => a + b;

console.log(add.length); // 预期是2,实际是0

let _curry = (fn) => {
  let len = fn.length
  function c(...args) {
    if(len === args.length) return fn(...args)
    else return c.bind(null, ...args)
  }
  return c
}

@lin2006yuo 06年的?

当执行 fn1()() 时,函数为什么返回
curry(sub_curry(function(){ return fn0() }))
啊,不是很理解

@EmiyaYang 这里打印的是 6 啊,为啥你写的是5?