最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [核心概念] 一文说透JS中的函数柯里化(Currying)

    正文概述 掘金(NeverMore奈文摩尔)   2021-01-07   544

    函数柯里化 (Currying)

    系列开篇

    面试题

    • 什么是柯里化
    • 柯里化的应用场景有哪些
    • 你了解函数式编程吗
    • 实现一个curring

    这是干什么的?

    先看看其他官方的各种定义引用:

    举个例子: 它是指将一个函数从可调用的 f(a, b, c) 转换为可以这样调用 f(a)(b)(c)。柯里化不会调用函数,它只是对函数进行转换

    我们先做个简单总结

    • 柯里化是一种函数式编程的技术
    • 只传递给函数一部分参数来调用它,并返回一个函数去处理剩下的参数。
    • 它不仅被用于 JavaScript,还被用于其他编程语言。

    分析理解

    上面我们下了个定义,但是我猜你还是对这个玩意不理解,有什么用,看分析你就明白了。

    javascript中函数调用一般长这样:

    let add = function(a, b) { 
      return a + b 
    }
    add(1, 2)                 //= 3
    

    一个函数接受一定数量的参数,然后执行后返回一个 value。当我们传入少的,或者多的参数(反正跟定义的不同数量),会造成结果不合预期(传少)或者多出来的参数被忽略的结果。

    add(1)                    //= NaN
    add(1, 2, 'ignore args')  //= 3
    

    下面我们使用curry的方式来把多参数加法变成一个个单参数调用的函数

    var curry = require('curry')  // 假设这个curry方法是外部引入的
    // 我们把这个函数柯里化, 就是把这个函数传到 curry() 中并返回出了一个被curry的函数 sum3
    var sum3 = curry(function(a, b, c) { 
      return a + b + c
    })
    // 那么接下来我们可以这样调用他们
    sum3(1, 2, 3)      //= 6     原来的方式当然ok
    sum3(1)(2)(3)      //= 6     可一个个参数调用
    sum3(1)(2, 3)      //= 6     
    sum3(1, 2)(3)      //= 6     也可传入部分,再传入剩余的
    

    sum3(1)(2, 3) 称为以偏函数partial的方式调用 或者称为 Partial Application (偏函数应用)

    这是指使用一个函数并将其应用一个或多个参数但不是全部参数,在这个过程中创建一个新函数。

    这样有啥好处,哪些场景可以用

    我们先空泛地说下好处,再解释

    一句话:令函数有更好的可读性灵活性复用性

    其他潜在好处:

    • 可以让你生成一个小型的,易于配置的函数库,而且这些函数的行为始终如一。(没有副作用的纯函数[相关概念])
    • 可以让你养成良好的函数命名习惯

    用些例子解释下:

    1. 参数复用,形成一些偏函数,灵活应用

    例如: 我们有一个用于格式化和输出信息的日志的函数 log(level, message)。假设长这样

    • level 设置日志警告等级 'warn', 'error', 'info'
    • message 日志内容信息
    function log(level, message) {
      console.log(`[${level}] ${message}`);
    }
    

    非常简单的函数, 想想平时都是这样调用是不是

    if (exp) {
      log('warn', 'sth... warn')
    }
    
    log('error', '...message')
    ...
    

    现在柯里化看看能有啥变化

    import _ from 'loadsh'
    var log = _.curry(log)
    
    // 柯里化之后,log 仍正常运行:
    log("warn", "some warn");  // "[warn] some warn"
    // 但是也可以以柯里化形式运行:
    log("warn")("some warn");  // "[warn] some warn"
    

    这样我们能创建更多『便捷函数』或者说偏函数

    let warnLogger = log('warn');
    
    // 使用它
    warnLogger("message");      // [warn] message
    

    warnLogger 是带有固定第一个参数的日志的偏函数,这个函数是参数固定的原来函数的部分函数。

    那么我们现在调用方式

    if (exp) {
      warnLogger('sth... warn')
    }
    errLogger('...err message')
    

    可读性是不是大大增加了,而且更灵活,因为你的这些小函数是可以互相组合的。 这里的例子因为简单,你还看不出好处有多大。尝试在复杂项目中使用,你会发现这种编程习惯会让你思路更清晰。

    2. 将操作原子化,方便单元测试

    简单来说,就是把各种小操作给工具函数化,增加了可读和复用性。

    还又很重要的一点是,这些函数可以非常方便地进行单元测试[关联概念]。

    举个例子:仅仅是个例子,请举一反三

    var objects = [{ id: 1 }, { id: 2 }, { id: 3 }]
    let idList = objects.map(function(item) { 
      return item.id 
    })
    

    其实我们要做的操作就是:遍历这个对象数组并取出他们的id

    那么现在我们可以用柯里化处理一波

    import _ from 'loadsh'
    
    var get = _.curry(function(prop, object) { 
      return object[prop] 
    })
    
    // map接受一个function 是用来获取每个对象的'prop'的 这里的prop是'id'
    objects.map(get('id'))    //= [1, 2, 3]
    

    我们在get函数中 真正创建的的可以部分配置的函数。 再看我们平时是不是使用这种方式多点,来创建一个方法用于获取对象数组中的id

    let getIDs = function(objects) {
        return objects.map(get('id'))
    }
    getIDs(objects)        //= [1, 2, 3]
    

    我们甚至可以进一步把 map也进行curry处理

    let curriedMap = curry(function(fn, value) { 
      return value.map(fn) 
    })
    var getIDs = curriedMap(get('id'))
    
    getIDs(objects)        //= [1, 2, 3]
    

    这样的代码读上去更清楚不是吗,当你在平时工作中积累了很多原子操作处理,就像一块块积木,顺手拿来,解决问题的速度会让你惊讶的。

    实际怎么写

    如果你在现实项目中想用curry,建议先了解函数式编程,自然而然地使用。单用curry也建议直接用lodash (省的自己写,而且别人的也处理了this等其他需要特殊处理的部分, placeholders也是很好用的)

    _.curry

    当然你也可以选择自己实现一个,根据下面原理,简单实现。

    原理是什么?

    了解原理实现,最好先了解 闭包【关联概念(强)】的概念。这是理解下面原理实现的前提。

    function myCurry(func) {
      // 我们myCurry调用应该返回一个包装器 curried,令这个函数curry化
      return function curried(...args) {
        // curry 的使用主要看参数数量
        return args.length >= func.length ?
          // 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,
          // 那么只需要将调用传递给它即可。直接现在就调用,返回函数结果
          func.call(this, ...args) :
          // 否则的话,返回另一个包装器方法,递归地调用curried,将之前传入的参数与新的参数拼接后一起传入。
          // 然后,在一个新的调用中,再次,我们将获得一个新的偏函数(如果参数不足的话),或者最终的结果。
          (...rest) => {
            return curried.call(this, ...args, ...rest);
          };
      };
    }
    
    

    注意每次调用参数不足返回包装器函数时,会将上一轮参数保存在词法环境中,利用闭包的特性,进行下一轮判断。

    我觉得看注释应该不用多解释了,不理解评论区留言吧。

    写完可以用这个例子测试下

    function sum(a, b, c) {
      return a + b + c;
    }
    
    let curriedSum = myCurry(sum);
    
    console.log( curriedSum(1, 2, 3) );  // 6,仍然可以被正常调用
    console.log( curriedSum(1)(2,3) );   // 6,对第一个参数的柯里化
    console.log( curriedSum(1)(2)(3) );  // 6,全柯里化
    

    其他

    为何使用函数式编程风格

    Pointfree 就是如何使用函数式编程的答案

    • 这就叫做 Pointfree:不使用所要处理的值,只合成运算过程。中文可以译作"无值"风格。
    • Pointfree 的本质就是使用一些通用的函数组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。

    例子

    下面是一个字符串,请问其中最长的单词有多少个字符

    var str = 'Lorem ipsum dolor sit amet consectetur adipiscing elit';
    

    我们先定义一些基本运算。

    // 以空格分割单词
    var splitBySpace = s => s.split(' ');
    
    // 每个单词的长度
    var getLength = w => w.length;
    
    // 词的数组转换成长度的数组
    var getLengthArr = arr => R.map(getLength, arr); 
    
    // 返回较大的数字
    var getBiggerNumber = (a, b) => a > b ? a : b;
    
    // 返回最大的一个数字
    var findBiggestNumber = 
      arr => R.reduce(getBiggerNumber, 0, arr);
    

    然后,把基本运算合成为一个函数

    var getLongestWordLength = R.pipe(
      splitBySpace,
      getLengthArr,
      findBiggestNumber
    );
    
    getLongestWordLength(str) // 11
    

    可以看到,整个运算由三个步骤构成,每个步骤都有语义化的名称,非常的清晰。这就是 Pointfree 风格的优势。

    memoization

    function memoizeFunction(func) {
        var cache = {};
        return function() {
            var key = arguments[0];
            if (cache[key]) {
                return cache[key];
            } else {
                var val = func.apply(this, arguments);
                cache[key] = val;
                return val;
            }
        };
    }
    
    var fibonacci = memoizeFunction(function(n) {
        return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
    });
    
    console.time('start1');
    fibonacci(100)
    console.timeEnd('start1')
    // 第二次有缓存
    console.time('start2');
    fibonacci(100)
    console.timeEnd('start2')
    

    [核心概念] 一文说透JS中的函数柯里化(Currying)

    缓存对计算速度提升效果明显


    上面这句话给你们,同样也给我自己前进的动力。

    我是摩尔,数学专业,做过互联网研发,测试,产品
    致力用技术改变别人的生活,用梦想改变自己的生活
    关注我,找到自己的互联网思路,踏实地打牢固自己的技术体系
    点赞、关注、评论、谢谢
    有问题求助可私信 1602111431@qq.com 我会尽可能帮助你

    参考

    • www.sitepoint.com/currying-in…
    • medium.com/@kevincenni…
    • hughfdjackson.com/javascript/…
    • juejin.cn/post/684490…
    • www.ruanyifeng.com/blog/2017/0…
    • zh.javascript.info/currying-pa…

    起源地下载网 » [核心概念] 一文说透JS中的函数柯里化(Currying)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元