专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  北京大学出的第二份 DeepSeek ... ·  昨天  
OSC开源社区  ·  RAG市场的2024:随需而变,从狂热到理性 ·  2 天前  
程序猿  ·  41岁DeepMind天才科学家去世:长期受 ... ·  2 天前  
程序员小灰  ·  清华大学《DeepSeek学习手册》(全5册) ·  3 天前  
OSC开源社区  ·  宇树王兴兴早年创业分享引围观 ·  5 天前  
51好读  ›  专栏  ›  SegmentFault思否

JavaScript 专题之函数柯里化

SegmentFault思否  · 公众号  · 程序员  · 2017-09-22 12:35

正文

JavaScript 专题系列第十三篇,讲解函数柯里化以及如何实现一个 curry 函数

定义

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

翻译成中文:

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

举个例子:

  1. function add(a, b) {

  2.    return a + b;

  3. }

  4. // 执行 add 函数,一次传入两个参数即可

  5. add(1, 2) // 3

  6. // 假设有一个 curry 函数可以做到柯里化

  7. var addCurry = curry(add);

  8. addCurry(1)(2) // 3

用途

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

举个例子:

  1. // 示意而已

  2. function ajax(type, url, data) {

  3.    var xhr = new XMLHttpRequest();

  4.    xhr.open(type, url, true);

  5.    xhr.send(data);

  6. }

  7. // 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余

  8. ajax('POST', 'www.test.com', "name=kevin")

  9. ajax('POST', 'www.test2.com', "name=kevin")

  10. ajax('POST', 'www.test3.com', "name=kevin")

  11. // 利用 curry

  12. var ajaxCurry = curry(ajax);

  13. // 以 POST 类型请求数据

  14. var post = ajaxCurry('POST');

  15. post('www.test.com', "name=kevin");

  16. // 以 POST 类型请求来自于 www.test.com 的数据

  17. var postFromTest = post( 'www.test.com');

  18. postFromTest("name=kevin");

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

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

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

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

举个例子:

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

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

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

  1. var name = person.map(function (item) {

  2.    return item.name;

  3. })

不过如果我们有 curry 函数:

  1. var prop = curry(function (key, obj) {

  2.    return obj[key]

  3. });

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

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

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

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

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

第一版

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

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

  1. // 第一版

  2. var curry = function (fn) {

  3.    var args = [].slice.call(arguments, 1);

  4.    return function() {

  5.        var newArgs = args.concat([].slice.call(arguments));

  6.        return fn.apply(this, newArgs);

  7.    };

  8. };

我们可以这样使用:

  1. function add(a, b) {

  2.     return a + b;

  3. }

  4. var addCurry = curry(add, 1, 2);

  5. addCurry() // 3

  6. //或者

  7. var addCurry = curry(add, 1);

  8. addCurry(2) // 3

  9. //或者

  10. var addCurry = curry(add);

  11. addCurry(1, 2) // 3

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

第二版

  1. // 第二版

  2. function sub_curry(fn) {

  3.    var args = [].slice.call(arguments, 1);

  4.    return function() {

  5.        return fn.apply(this , args.concat([].slice.call(arguments)));

  6.    };

  7. }

  8. function curry(fn, length) {

  9.    length = length || fn.length;

  10.    var slice = Array.prototype.slice;

  11.    return function() {

  12.         if (arguments.length < length) {

  13.            var combined = [fn].concat(slice.call(arguments));

  14.            return curry(sub_curry.apply(this, combined), length - arguments.length);

  15.        } else {

  16.             return fn.apply(this, arguments);

  17.        }

  18.    };

  19. }

我们验证下这个函数:

  1. var fn = curry(function(a, b, c) {

  2.    return [a, b, c];

  3. });

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

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

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

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

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

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

  1. function sub_curry(fn){

  2.     return function(){

  3.        return fn()

  4.    }

  5. }

  6. function curry(fn, length){

  7.    length = length || 4;

  8.    return function(){

  9.         if (length > 1) {

  10.            return curry(sub_curry(fn), --length)

  11.        }

  12.        else {

  13.             return fn()

  14.        }

  15.    }

  16. }

  17. var fn0 = function(){

  18.    console.log(1)

  19. }

  20. var fn1 = curry(fn0)

  21. fn1()()()() // 1

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

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

  1. curry(sub_curry(fn0))

  2. // 相当于

  3. curry(function(){

  4.    return fn0()

  5. })

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

  1. curry(sub_curry(function(){

  2.     return fn0()

  3. }))

  4. // 相当于

  5. curry( function(){

  6.    return (function(){

  7.        return fn0()

  8.    })()

  9. })

  10. // 相当于

  11. curry( function(){

  12.    return fn0()

  13. })

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

  1. // 跟 fn1()() 的分析过程一样

  2. curry(function(){

  3.    return fn0()

  4. })

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

  1. fn()

  2. // 相当于

  3. (function(){

  4.    return fn0()

  5. })()

  6. // 相当于

  7. fn0()

  8. // 执行 fn0 函数,打印 1

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

  1. var fn0 = function(a, b, c, d) {

  2.    return [a, b, c, d];

  3. }

  4. var fn1 = curry(fn0);

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

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

  1. fn1( "a", "b")

  2. // 相当于

  3. curry(fn0)("a", "b")

  4. // 相当于

  5. curry(sub_curry(fn0, "a", "b"))

  6. // 相当于

  7. // 注意 ... 只是一个示意,表示该函数执行时传入的参数会作为 fn0 后面的参数传入

  8. curry(function(...){

  9.    return fn0("a", "b", ...)

  10. })

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

  1. curry(sub_curry(function(...){

  2.    return fn0("a", "b", ...)

  3. }), "c")

  4. // 相当于

  5. curry(function(...){

  6.    return (function(...) {return fn0("a", "b", ...)})("c")

  7. })

  8. // 相当于

  9. curry(function(...){

  10.     return fn0("a", "b", "c", ...)

  11. })

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

  1. ( function(...){

  2.    return fn0("a", "b", "c", ...)

  3. })("d")

  4. // 相当于

  5. fn0("a", "b", "c", "d")

函数执行结束。

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

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

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

更易懂的实现

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

  1. function curry(fn, args) {

  2.    length = fn.length;

  3.    args = args || [];

  4.    return function() {

  5.        var _args = args.slice(0),

  6.            arg, i;

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

  8.            arg = arguments[i];

  9.            _args.push(arg);

  10.        }

  11.         if (_args.length < length) {

  12.            return curry.call(this, fn, _args);

  13.        }

  14.        else {

  15.             return fn.apply(this, _args);

  16.        }

  17.    }

  18. }

  19. var fn = curry(function(a, b, c) {

  20.    console.log([a, b, c]);

  21. });

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

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

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

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

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

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

第三版

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

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

  1. var fn = curry(function(a, b, c) {







请到「今天看啥」查看全文