mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。
30.75k stars 4.71k forks source link

JavaScript专题之函数柯里化 #42

Open mqyqingfeng opened 7 years ago

mqyqingfeng commented 7 years ago

定义

维基百科中对柯里化 (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,对作者也是一种鼓励。

liuxinqiong commented 7 years ago

日常学习

FrontToEnd commented 7 years ago

好难理解啊☹️

mqyqingfeng commented 7 years ago

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

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

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

mqyqingfeng commented 7 years ago

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

yangchongduo commented 7 years ago

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

yangchongduo commented 7 years ago
var name = person.map(function (item) {
    return item.name;
})
const fn = function (item) {
    return item.name;
}
person.map(fn)

PS:这样也可以参数复用

mqyqingfeng commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

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

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

heyunjiang commented 7 years ago

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

yangchongduo commented 7 years ago

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); } }

LiuYashion commented 7 years ago

值得一看~

hlmjack commented 7 years ago

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

mqyqingfeng commented 7 years ago

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

fi3ework commented 7 years ago

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

mqyqingfeng commented 6 years ago

@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 对象

xiaobinwu commented 6 years ago

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

bepromising commented 6 years ago

我有个问题想请教下。 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函数呢。

mqyqingfeng commented 6 years ago

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

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

BugHshine commented 6 years ago

柯里化 是真的绕啊

mqyqingfeng commented 6 years ago

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

izuomeng commented 6 years ago

第三版的代码是不是有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)
    }
  }
}

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

mqyqingfeng commented 6 years ago

@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
izuomeng commented 6 years ago

少了一行,抱歉😄

// 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 = []
      }
  }
}
mqyqingfeng commented 6 years ago

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

izuomeng commented 6 years ago

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

mqyqingfeng commented 6 years ago

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

Pa-wN commented 6 years ago

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

iiicon commented 6 years ago

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

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

mqyqingfeng commented 6 years ago

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

olvvlo commented 6 years ago
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

mqyqingfeng commented 6 years ago

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

flymie commented 6 years ago

对于博主第二版的极简版的解释有点不太理解。 当执行 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’
mqyqingfeng commented 6 years ago

@flymie 不会执行呀

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

这时候不会执行呀~

flymie commented 6 years ago

@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,感觉就像函数执行过一样。
})
Tan90Qian commented 6 years ago

@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)也才会触发

xiaoyanhao commented 6 years ago

函数的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)将会报错。

dreamerhammer commented 6 years ago

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)的情况。

NihilityT commented 6 years ago
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
hugeface commented 6 years ago

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

wuyunjiang commented 5 years ago

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

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

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

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

这里test.length=2

SunLn commented 5 years ago

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 三个参数而出现问题

xiaohesong commented 5 years ago

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

zalatmza commented 5 years ago

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

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
inottn commented 5 years ago

@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)

xiaohesong commented 5 years ago

@inottn 嗯是的,看错了。

shuzewu commented 5 years ago

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


const curry = fn =>
   judge = (...args) => 
      args.length === fn.length
         ? fn(...args)
         : (...arg) => judge(...args, ...arg)
lockontan commented 5 years ago

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

CoolRice commented 5 years ago

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

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

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

TanMichael commented 5 years ago

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')时才能跟之前一样