add(1)(2)(3)(4)() == 10

最近面试的时候遇到了一个面试题:add(1)(2)(3)(4)输出结果为10。

const addFn = (...args) => args.reduce((total, cur) => total + cur, 0)

const curry = (fn) => {
// Your Code Here
}

const add = curry(addFn)
const value = add(1,2)(3)(4)()
console.log(value) // 10

看到这道面试题的时候,有点迷茫,不知所措~~~

就像是宝强在《人在囧途》中的反应:啥!啥!啥!这写的都是啥?


一开始,我发现1+2+3+4=10,写了以下的代码

function add (a, b, c, d) {
    return a + b + c + d
}
add(1, 2, 3, 4) // 10

add(1,2)(3)(4)()//10
function add (a) {
    return function (b) {
        return function (c) {
            return function (d) {
                return a + b + c + d
            }
        }
    }
}

又觉得不会这么简单吧,要是累加到100、1000呢~

没有思路~

面试结束后,在网上找相关的知识点学习,了解到一个概念叫:函数柯里化


什么是函数柯里化(curry)

函数柯里化(curry)是函数式编程里面的概念。

curry的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

简单点来说就是:每次调用函数时,它只接受一部分参数,并返回一个函数,直到传递所有参数为止。

举个🌰 将下面接受两个参数的函数改为接受一个参数的函数。

const add = (x, y) => x + y;
add(1, 2);

改成每次只接受一个参数的函数

const add = x => y => x + y;
add(1)(2);`

我们可以自己先尝试写一个add(1)(2)(3)

const add = x => y => z => x + y + z;
console.log(add(1)(2)(3));

看起来并不是那么难,但是如果面试官的要求是实现一个add 函数,同时支持下面这几种的用法呢

add(1, 2, 3);
add(1, 2)(3);
add(1)(2, 3);

如果还是按照上面的这种思路,我们是不是要写很多种呢...

我们当然可以自己实现一个工具函数专门来生成柯里化函数。

主要思路是什么呢,要判断当前传入函数的参数个数 (args.length) 是否大于等于原函数所需参数个数 (fn.length) ,如果是,则执行当前函数;如果是小于,则返回一个函数。

const curry = (fn, ...args) => 
    // 函数的参数个数可以直接通过函数数的.length属性来访问
    args.length >= fn.length // 这个判断很关键!!!
    // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
    ? fn(...args)
    /**
     * 传入的参数小于原始函数fn的参数个数时
     * 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
    */
    : (..._args) => curry(fn, ...args, ..._args);

function add1(x, y, z) {
    return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));

柯里化有什么作用

主要有3个作用: 参数复用提前返回延迟执行

我们来简单的解释一下: 参数复用:拿上面 f这个函数举例,只要传入一个参数 z,执行,计算结果就是 1 + 2 + z 的结果,1 和 2 这两个参数就直接可以复用了。

提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。


函数柯里化解决方案

函数柯里化有两种不同的场景,一种为函数参数个数定长的函数,另外一种为函数参数个数不定长的函数。

函数参数个数定长的柯里化解决方案
// 定长参数
function add (a, b, c, d) {
    return [
      ...arguments
    ].reduce((a, b) => a + b)
}

function currying (fn) {
    let len = fn.length
    let args = []
    return function _c (...newArgs) {
        // 合并参数
        args = [
            ...args,
            ...newArgs
        ]
        // 判断当前参数集合args的长度是否 < 目标函数fn的需求参数长度
        if (args.length < len) {
            // 继续返回函数
            return _c
        } else {
            // 返回执行结果
            return fn.apply(this, args.slice(0, len))
        }
    }
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同时支持addCurry(1)(2, 3)(4)该方式调用
console.log(total) // 10
函数参数个数不定长的柯里化解决方案

问题升级:那这个问题再升级一下,函数的参数个数不确定时,如何实现呢?

function add (...args) {
    return args.reduce((a, b) => a + b)
}

function currying (fn) {
    let args = []
    return function _c (...newArgs) {
        if (newArgs.length) {
            args = [
                ...args,
                ...newArgs
            ]
            return _c
        } else {
            return fn.apply(this, args)
        }
    }
}

let addCurry = currying(add)
// 注意调用方式的变化
console.log(addCurry(1)(2)(3)(4, 5)())

函数柯里化是一种技术,一种将多入参函数变成单入参函数

这样做会让函数变得更复杂,但同时也提升了函数的普适性。

举个例子

//正常函数
function sum(a,b){
  console.log(a+b); 
}

sum(1,2);    //输出3
sum(1,3);    //输出4

//柯里化函数
function curry(a){
    return (b) =>{
        console.log(a+b)
    } 
}

const sum = curry(1);

sum(2);  //输出3
sum(3);  //输出4

例子里,为使用柯里化的函数在入参的时候即使在某一个入参是固定的情况下。也需要一样的去输入,那么这个输入就变得冗余了。

柯里化之后的函数可以省略掉一个固定的入参。

但到这里,还有一个问题。现在只是一层封装的柯里化。如果是四层,五层呢。

假设有这样一个场景

//柯里化之前
function sum(a,b,c,d,e){
    console.log(a+b+c+d+e)
}
sum(1,2,3,4,5);
//柯里化
function sum1(a){
    return function sum2(b){
        return function sum3(c){
             return function sum4(d){
                 return function sum5(e){
                    console.log(a+b+c+d+e)
                 }
             }
        }
    }
}

sum1(1)(2)(3)(4)(5);

多层柯里化的时候代码会不美观,可读性非常差。

但需求总是在的。我们总会需要多层柯里化的时候。

所有,我们可以封装一个函数来帮助我们完成函数向柯里化的转换。

 //函数柯里化封装(这个封装可以直接复制走使用)
    function curry(fn, args) {
            var length = fn.length;
            var args = args || [];
            return function () {
                newArgs = args.concat(Array.prototype.slice.call(arguments));
                if (newArgs.length < length) {
                    return curry.call(this, fn, newArgs);
                } else {
                    return fn.apply(this, newArgs);
                }
            }
        }
        
        //需要被柯里化的函数
        function multiFn(a, b, c) {
            return a * b * c;
        }
        
        //multi是柯里化之后的函数
        var multi = curry(multiFn);
        console.log(multi(2)(3)(4));
        console.log(multi(2, 3, 4));
        console.log(multi(2)(3, 4));
        console.log(multi(2, 3)(4));

柯里化的应用场景

其实柯里化大多是情况下是为了减少重复传递的不变参数。

举个最简单的例子吧。手机号正则校验。

//校验手机号
function validatePhone(regExp,warn,phone){
  const reg = regExp;
  if (phone && reg.test(phone) === false) {
    return Promise.reject(warn);
  }
  return Promise.resolve();
}

//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",187****3311)

这种写法乍一看好像没什么问题。但是,如果你需要多次调用呢?

//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",137****1234)
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",159****6204)
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",137****2125)
//调用校验
validatePhone(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符",191****5236)

会发现,正则和提示入参是固定的。很冗余。

我们可以使用我们上面封装的柯里化工具(curry函数)进行如下修改。

//完成柯里化
const curryValid = curry(validatePhone);
const validatePhoneCurry  =curryValid(/^(13[0-9]|14[0-9]|15[0-9]|166|17[0-9]|18[0-9]|19[8|9])\d{8}$/,"手机号格式不符");

//调用柯里化之后的函数
validatePhoneCurry(159****6204);
validatePhoneCurry(137****1234);
validatePhoneCurry(137****2125);
validatePhoneCurry(191****5236);

如上,我们可以省略很多不必要的参数。

当然,柯里化的应用场景还有延时执行(闭包也可以实现,而且更简单)还有提前返回(主要针对IE,IE也马上退休了,这里不认为有赘述的意义)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容