New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript专题之函数柯里化 #42

Open
mqyqingfeng opened this Issue Aug 11, 2017 · 39 comments

Comments

Projects
None yet
@mqyqingfeng
Owner

mqyqingfeng commented Aug 11, 2017

定义

维基百科中对柯里化 (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

This comment has been minimized.

Show comment
Hide comment
@liuxinqiong

liuxinqiong Aug 18, 2017

日常学习

liuxinqiong commented Aug 18, 2017

日常学习

@FrontToEnd

This comment has been minimized.

Show comment
Hide comment
@FrontToEnd

FrontToEnd Aug 22, 2017

好难理解啊☹️

FrontToEnd commented Aug 22, 2017

好难理解啊☹️

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Aug 22, 2017

Owner

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

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

mqyqingfeng commented Aug 22, 2017

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

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

This comment has been minimized.

Show comment
Hide comment
@jawil

jawil Aug 22, 2017

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

jawil commented Aug 22, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Aug 22, 2017

Owner

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

Owner

mqyqingfeng commented Aug 22, 2017

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

@yangchongduo

This comment has been minimized.

Show comment
Hide comment
@yangchongduo

yangchongduo Oct 10, 2017

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

yangchongduo commented Oct 10, 2017

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

@yangchongduo

This comment has been minimized.

Show comment
Hide comment
@yangchongduo

yangchongduo Oct 10, 2017

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

PS:这样也可以参数复用

yangchongduo commented Oct 10, 2017

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

PS:这样也可以参数复用

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Oct 11, 2017

Owner

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

Owner

mqyqingfeng commented Oct 11, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Oct 11, 2017

Owner

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

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

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

Owner

mqyqingfeng commented Oct 11, 2017

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

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

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

@heyunjiang

This comment has been minimized.

Show comment
Hide comment
@heyunjiang

heyunjiang Oct 11, 2017

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

heyunjiang commented Oct 11, 2017

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

@yangchongduo

This comment has been minimized.

Show comment
Hide comment
@yangchongduo

yangchongduo Oct 19, 2017

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

yangchongduo commented Oct 19, 2017

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

This comment has been minimized.

Show comment
Hide comment
@LiuYashion

LiuYashion Oct 31, 2017

值得一看~

LiuYashion commented Oct 31, 2017

值得一看~

@hlmjack

This comment has been minimized.

Show comment
Hide comment
@hlmjack

hlmjack Oct 31, 2017

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

hlmjack commented Oct 31, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Oct 31, 2017

Owner

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

Owner

mqyqingfeng commented Oct 31, 2017

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

@fi3ework

This comment has been minimized.

Show comment
Hide comment
@fi3ework

fi3ework Nov 3, 2017

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

fi3ework commented Nov 3, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Nov 8, 2017

Owner

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

Owner

mqyqingfeng commented Nov 8, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@xiaobinwu

xiaobinwu Nov 9, 2017

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

xiaobinwu commented Nov 9, 2017

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

@bepromising

This comment has been minimized.

Show comment
Hide comment
@bepromising

bepromising Nov 25, 2017

我有个问题想请教下。
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 commented Nov 25, 2017

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

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Nov 27, 2017

Owner

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

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

Owner

mqyqingfeng commented Nov 27, 2017

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

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

@BugHshine

This comment has been minimized.

Show comment
Hide comment
@BugHshine

BugHshine Dec 1, 2017

柯里化 是真的绕啊

BugHshine commented Dec 1, 2017

柯里化 是真的绕啊

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Dec 5, 2017

Owner

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

Owner

mqyqingfeng commented Dec 5, 2017

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

@izuomeng

This comment has been minimized.

Show comment
Hide comment
@izuomeng

izuomeng Dec 6, 2017

第三版的代码是不是有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 commented Dec 6, 2017

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

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Dec 7, 2017

Owner

@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

Owner

mqyqingfeng commented Dec 7, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@izuomeng

izuomeng Dec 7, 2017

少了一行,抱歉😄

// 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 commented Dec 7, 2017

少了一行,抱歉😄

// 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

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Dec 7, 2017

Owner

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

Owner

mqyqingfeng commented Dec 7, 2017

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

@izuomeng

This comment has been minimized.

Show comment
Hide comment
@izuomeng

izuomeng Dec 7, 2017

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

izuomeng commented Dec 7, 2017

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Dec 7, 2017

Owner

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

Owner

mqyqingfeng commented Dec 7, 2017

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

@Pa-wN

This comment has been minimized.

Show comment
Hide comment
@Pa-wN

Pa-wN Dec 23, 2017

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

Pa-wN commented Dec 23, 2017

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

@iiicon

This comment has been minimized.

Show comment
Hide comment
@iiicon

iiicon Feb 2, 2018

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

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

iiicon commented Feb 2, 2018

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

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

@mqyqingfeng

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Feb 8, 2018

Owner

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

Owner

mqyqingfeng commented Feb 8, 2018

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

@Wiolem

This comment has been minimized.

Show comment
Hide comment
@Wiolem

Wiolem Mar 4, 2018

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 commented Mar 4, 2018

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

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Mar 8, 2018

Owner

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

Owner

mqyqingfeng commented Mar 8, 2018

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

@flymie

This comment has been minimized.

Show comment
Hide comment
@flymie

flymie Apr 12, 2018

对于博主第二版的极简版的解释有点不太理解。
当执行 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 commented Apr 12, 2018

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

This comment has been minimized.

Show comment
Hide comment
@mqyqingfeng

mqyqingfeng Apr 13, 2018

Owner

@flymie 不会执行呀

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

这时候不会执行呀~

Owner

mqyqingfeng commented Apr 13, 2018

@flymie 不会执行呀

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

这时候不会执行呀~

@flymie

This comment has been minimized.

Show comment
Hide comment
@flymie

flymie Apr 16, 2018

@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 commented Apr 16, 2018

@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

This comment has been minimized.

Show comment
Hide comment
@Tan90Qian

Tan90Qian Apr 19, 2018

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

Tan90Qian commented Apr 19, 2018

@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

This comment has been minimized.

Show comment
Hide comment
@xiaoyanhao

xiaoyanhao May 10, 2018

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

xiaoyanhao commented May 10, 2018

函数的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

This comment has been minimized.

Show comment
Hide comment
@dreamerhammer

dreamerhammer Aug 8, 2018

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

dreamerhammer commented Aug 8, 2018

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

This comment has been minimized.

Show comment
Hide comment
@NihilityT

NihilityT Sep 30, 2018

var curry = function (fn) {
	var _arguments = arguments;
	return function () {
		var args = Array.prototype.slice.call(_arguments);
		var i, next, has_placeholder = false;

		next = args.indexOf(curry._);
		next = next == -1 ? args.length : next;

		for (i = 0; i < arguments.length; i++) {
			if (arguments[i] === curry._)
				has_placeholder = true;

			args[next] = arguments[i];

			next = args.indexOf(curry._, next + 1);
			next = next == -1 ? args.length : next;
		}

		fn = args[0];
		if (has_placeholder ||
		    next < 1 + fn.length ||
		    next < args.length)
			return curry.apply(this, args);
		else
			return fn.apply(this, args.slice(1));
	};
};
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 empty_c = curry();
var cubic = empty_c(Math.pow, _, 3);
cubic(2);
// 8

NihilityT commented Sep 30, 2018

var curry = function (fn) {
	var _arguments = arguments;
	return function () {
		var args = Array.prototype.slice.call(_arguments);
		var i, next, has_placeholder = false;

		next = args.indexOf(curry._);
		next = next == -1 ? args.length : next;

		for (i = 0; i < arguments.length; i++) {
			if (arguments[i] === curry._)
				has_placeholder = true;

			args[next] = arguments[i];

			next = args.indexOf(curry._, next + 1);
			next = next == -1 ? args.length : next;
		}

		fn = args[0];
		if (has_placeholder ||
		    next < 1 + fn.length ||
		    next < args.length)
			return curry.apply(this, args);
		else
			return fn.apply(this, args.slice(1));
	};
};
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 empty_c = curry();
var cubic = empty_c(Math.pow, _, 3);
cubic(2);
// 8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment