欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

你不知道的JS专栏 - 函数柯里化(柯里化含义, 作用, 写法, 大厂面试题)

程序员文章站 2022-07-14 14:25:40
...

你不知道的JS - 函数柯里化

目录:

  • 柯里化函数的概览

  • 手撕柯里化

    • 阿里面试题

    • 柯里化的写法

  • 柯里化函数的好处和作用

    • 参数复用

    • 延迟执行

    • 柯里化的应用举例

柯里化函数的概览

(如果你已经对柯里化有了一个比较好的了解, 也知道柯里化的写法, 只是对柯里化的作用和应用场景抱有一定的疑问的话, 我建议你直接看第三节柯里化的作用)

这哥们就是说让你把本来一次性传几个参数的函数分成好几次传参的方式最终执行

官方定义: 将使用多个参数的函数转换成一系列使用一个参数的函数的技术

这到底是啥意思呢看个例子你就明白了

function add(x, y, z) {
    return x + y + z; 
}

// 正常调用我们是
add(1, 2, 3); // 输出6

// 就上面这个累加函数, 我们用柯里化函数来包装一下他然后他就可以像下面这样执行
function curry(func) {}; // 假装柯里化函数已经写好了
add = curry(add); // 转换一下
add(1)(2)(3); // 输出6

很多朋友会说我为什么要进行柯里化啊, 跟神经一样的操作, 去百度搜一个柯里化到底有什么用给我出一大堆博客, 博客一打开就是*说柯里化是xxxxx, 半天没看明白到底为什么用柯里化

其实用柯里化可以有几个好处, 由于我们这里还没有正儿八经的学习柯里化, 我先给你渗透一个好处

你给我仔细想想Object.prototype.bind方法, 没错就是改变this指向的这哥们, 你有没有发现这哥们给你来了
一个神奇的操作

call和apply都给你一调用就执行了, 而bind却是返回给你一个新的函数, 等你想什么时候执行再去执行, 你仔细想想bind不就是柯里化吗, 柯里化的定义是什么, 把本来一次性要传完的参数分成几次传递, 你看我下面的实例newFoo是不是传了两次参数才出的结果, 所以bind就是柯里化函数

通过这里, 我想你应该可以发现柯里化的第一个好处就是可以延迟函数的运行时间, 不用马上就执行

function foo(x, y, z) { console.log(this.name); console.log(x, y, z)};
foo.call(window, 2 ,2 ,2); // '' 2, 2, 2 
foo.apply({name: 'loki'}, [3, 3, 3]); // loki, 3, 3, 3

let newFoo = foo.bind({name:'thor'}, 1); 
newFoo(2, 3); // thor [1, 2, 3]

到这你看不懂不能理解都没关系, 但是这只是个概览, 让你混个眼熟, 我希望你往下看

手撕柯里化

OK经过上面我相信你已经基本认识柯里化到底他的官方定义是什么了, 但是即使我给你列举了bind的例子, 你可能还是无法理解柯里化, 因为你根本不知道他到底是为了解决什么问题的, 即使是bind他又是解决什么问题呢? 我们为什么要延迟函数执行时间呢? 小小的脑袋装着大大的问号, 这一块我用一些实例来告诉你柯里化到底会应用于哪些场景, 及有些操作为什么要这么做, 在这一节里我给你答案

要了解柯里化, 我们至少要先知道柯里化怎么写

  • 阿里面试题

    看看阿里2017年的秋招面试题,如何实现一个add(1)(2)(3) = 6

    实现add(1)(2)(3)我们知道这个add函数被分开调用了三次,每次传入一个参数, 当第三个参数传入的时候,返回了结果, 那么我们其实可以得出几点结论:

    1. 用过jquery我们就知道, 如果想要这样进行链式操作, 势必在每单次执行以后返回了这个函数本身

    2. 当add(1)执行的时候函数没有计算值出来, 但是1这个参数却在本次执行中被保留了下来并且在最后一次3传入的时候被拿出来进行累加, 那么函数执行完毕作用域销毁, 这个1跟2应该是跳出了函数作用域进行了生命周期的延长, 所以肯定进行了闭包操作

    3. 能够恰好在第三个函数传递进来就真正得到执行, 那么内部肯定对实参的个数进行了计算

    根据这三点, 我们可以摸索着来写写

    const add = (function () {
        var _args = []; // 这个_args用来存储所有传入的形参
        return function () {
            if (arguments.length) { // 如果形参存在就直接进行循环, 不存在直接返回函数
                for (let i = 0, len = arguments.length; i < len; i++) {
                    _args.push(arguments[i]) // 循环形参, 将他们一一放入_args中
                    console.log(_args);
                    if (_args.length === 3) { 
                        // 但是每添加一次要记得判断一下_args.length是不是等于3了
                        // 一旦_args等于3了就要真正的去进行计算并且将值返回了
                        let num = 0;
                        _args.forEach(ele => num += ele)
                        return num;
                    }
                }
                return add;
            }
        }
    }())
    
    console.log(add(1)(2)(3)); // 输出6
    
  • 手写柯里化
    上面我们确实实现了一个比较简单的add(1)(2)(3) = 6的效果, 但是我们知道这个是无法复用的, 我们渴望有一个Curry函数, 当我们将一个需要进行柯里化的函数传递进Curry函数中后, curry函数会返回一个新的函数给我们, 这个新的函数就可以进行柯里化操作了

    function curry(func) {} // 假设我们已经写好了curry函数
    
    // 这是正儿八经的累加函数
    function add(x, y, z) {
        return x + y + z; 
    }
    
    // 当我们将add方法传入curry方法中得到一个新的addCurry时
    // addCurry将可以实现addCurry(1)(2)(3)的效果
    const addCurry = curry(add);
    addCurry(1)(2)(3); // 6
    //我们还可以进行随意搭配比如
    // addCurry(1)(2,3); // 6
    // addCurry(1, 2)(3); // 6
    

    OK, 那么这个curry函数我们到底应该怎么来写

    1. 首先, 它接收一个函数作为参数, 最终肯定也是返回了一个函数

    2. 同时传入的这个函数一定要是固定了形参个数的函数

    function curry(func, argNum) {
            // func: 传递进来的要进行柯里化的方法
            // length: 总共有多少个形参
            const length = argNum || func.length; // 直接拿到形参的最大个数
            let _args = []; // 定义的用来缓存每一次传入的参数数组
            return function () {
                // 本次是否真的传输了参数
                if (arguments.length != 0) {
                    console.log(arguments);
                    // 如果真的存在, 那么我们开始计算
                    for (var i = 0, len = arguments.length; i < len; i++) {
                        _args.push(arguments[i]);
                        if (_args.length === length) {
                            return func(..._args);
                        }
                    }
                }
                // 如果没有传输参数进来或者参数不满
                return arguments.callee;
            }
        }
    
        function add(x, y, z) {
            return x + y + z;
        }
    
        var newAdd = curry(add);
        // console.log(newAdd(1)(2)(3)); 6
        // console.log(newAdd(1)(2, 3)); 6
        console.log(newAdd(1, 2)(3));  // 6
        
    

    大致操作差不多, 只是我们提取公共的curry方法让其会更加的灵活

    OK, 关于柯里化的写法我是已经都写出来了, 希望你理解它的这个写法, 我们接下来来看看, 柯里化的作用及在实际项目中的一些使用

柯里化的好处和作用

上面花了大篇幅介绍柯里化, 可能也仅仅是让大家明白了柯里化的写法和概念, 而柯里化带来的作用和好处其实我写的还不够详细, 那么这里我将通过一些实例来写写柯里化的好处和具体场景

我来写写柯里化比较显著的两个优势:

  • 参数复用, 减少重复代码

    所谓参数复用, 就是缓存之前传入的参数, 让他不被释放掉, 从而不用进行重复传参, 我们来写写

    来看个实例

    // ajax请求大家都玩过, 我这里不再进行ajax的封装, 而是假装自己已经写好了ajax, 进行请求
    ajax(methods = 'GET', flag, url, data = {},  cb) {}; // 假设已经写好了
    ajax('POST', true, '/api/getData', {name: 'loki', }, cb1); 
    ajax('POST', true, '/api/getData', {id: '1010', }, cb2); 
    ajax('POST', true, '/api/getTel', {tel: '1234',key: 10 }, cb3); 
    ajax('POST', true, '/api/getTel', {arr: [1, 2, 3] }, cb4);
    
    // 我们总是在进行很多的ajax请求, 上放发送了四次POST请求, 我们明显发现
    // 代码的冗余非常高, 重复代码非常的多, 特别是那个POST和那个true, 看的人头大
    // 这时候我们可以使用柯里化来解决
    
    const ajaxCurry = curry(ajax);
    var post = ajaxCurry('POST', true);
    post('/api/getData', {name: 'loki', }, cb1);
    post('/api/getData', {id: '1010', }, cb2);
    post('/api/getTel', {tel: '1234',key: 10 }, cb3);
    post('/api/getTel', {arr: [1, 2, 3] }, cb4);
    
    // 至少我们把POST和true就搞定了, 其实你还可以继续减少重复度, 
    // 但是我怕有的朋友看不懂了, 所以就不写了
    
  • 延迟运行

    延迟运行这个看看bind方法就知道了, 我上面应该写过了

    function foo(x, y, z) { console.log(this.name); console.log(x, y, z)};
    foo.call(window, 2 ,2 ,2); // '' 2, 2, 2 
    foo.apply({name: 'loki'}, [3, 3, 3]); // loki, 3, 3, 3
    
    let newFoo = foo.bind({name:'thor'}, 1); 
    newFoo(2, 3); // thor [1, 2, 3]
    

    bind函数从来不用马上就被执行, 这给了我们某些操作的空间

  • 柯里化的应用实例

    我们来看一个需求, 我们公司有一个记账系统, 每天的钱都会入账, 而公司每个月底都会把账目列出来, 计算这个月到底入账了多少钱,这个需求你怎么写

    // 我们先来看看普通写法会有哪些问题

    // 为了避免全局变量的问题, 我们必须封闭作用域
    const addMoney = (function() {
        let sum = 0; // 初始入账值为0
        function caculateMoney(money) {
            sum += money;
            return sum;
        }
        return function(everyDayMoney) {
            return caculateMoney(everyDayMoney); 
        }
    }())
    
    // 然后我们每天都会进行钱的累计, 这种写法可不可以呢, 确实可以
    // 但是却有一点的小瑕疵
    // 1. 过多的立即执行函数其实会让代码变得难以阅读
    // 2. 我们其实追求的实际上是月底的那个累加值, 
    // 而当前我们必须每天都必须计算一次运行一次caculate方法
    // 造成了不必要的浪费
    

    柯里化其实可以很好的帮我们解决上面的问题, 柯里化可以进行缓冲, 所以我们不必每天都计算一次money, 我们可以等到每个月的最后一天到来, 再进行最终的计算

    // 柯里化代码curry如上, 就不在重新书写了
        function caculateMoney(...moneyArr) {
        console.log(moneyArr);
        let num = 0;
        moneyArr.forEach(ele => num += ele);
        return num;
    }
    
    const caculateMoneyCurry = curry(caculateMoney, 5);
    
    console.log(caculateMoneyCurry(100)(10)(20)(30)(5)); // 假设我们计算5天的工资,这里会输出165
    

    代码经过改写以后, 相较之前其实代码的风格已经有很大的改变, 也多了几点优势

    1. 代码的观赏性很强, 没有使用过多的立即执行函数, 也很简洁

    2. 代码的可读性变得更强了, 在之前的代码中, 我们其实比较难知道当月到底计算了多少天的, 而柯里化过后我们可以很明显的发现我们计算了多少天的入账金额

    3. 不用频繁的触发caculateMoney方法, 节约了部分的性能

    本来想多写一些实例的, 突然又觉得篇幅太大不太好, 所以这个对比实例就只举一个了,你在下一节会看到更多的柯里化应用场景

OK, 柯里化函数到此为止, 希望我写清楚了, 也希望可以帮助到大家

相关标签: 你不知道的JS