壹 ❀ 引

很久之前看到过的一道面试题,最近复习又遇到了,这里简单做个整理,本题考点主要是函数柯里化,所以在实现前还是简单介绍什么是柯里化。

贰 ❀ 函数柯里化(Currying)

所谓函数柯里化,其实就是把一个接受多个参数的函数,转变成接受一个单一参数,且返回接受剩余参数并能返回结果的新函数的技术。举个最简单的例子:

const add = (a, b) => a + b;
add(1, 2);

add是一个求和的函数,它接受2个参数,但假设我们将其变为柯里化函数,它应该接受一个参数,并返回一个接受剩余参数,且依旧能求出相同结果的函数:

const add = () => {//...};
// 接受第一个参数且返回了一个新函数
const add_ = add(1);
// 新函数接受剩余参数,最终得出最终结果
add_(2);
// 简约来写就是
add(1)(2);

说到底,函数柯里化的概念其实也离不开闭包,函数A接受一个参数(闭包中的自由变量)且返回一个新函数B(闭包),而函数A明明已执行并释放,当函数B执行时依旧能访问A函数当时所接参数。

叁 ❀ 实现add(1)(2)(3)

只要将闭包的概念带入进来,我们将上面的add改写为柯里化就非常简单了,如下:

const addCurry = (a) => (b) => a + b;

// 等同于
const addCurry = function (a) {
return function (b) {
return a + b;
}
};
addCurry(1)(2);

可以看到在执行到内部函数时,作用域很明确的标明了这是一个闭包,且访问了自由变量a;

那么回到题目add(1)(2)(3)怎么实现呢?还是一样的,既然你能调用3次,说明函数内部等嵌套返回2次函数,比如:

const addCurry = (a) => (b) => (c) => a + b + c;
console.log(addCurry(1)(2)(3));// 6 // 等同于
const addCurry = function (a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}

肆 ❀ 固定形参的任意实参

上面关于add(1)(2)(3)实现中,我们其实是固定了一次传递一个参数,那么现在我们将问题升级,需要定义一个柯里化函数,它能接受任意数量的参数,比如:

addCurry(1, 2, 3);
addCurry(1)(2)(3);
addCurry(1, 2)(3);
addCurry(1)(2, 3);

怎么做?对于addCurry函数自身而言,我们确定它最多同时接受3个参数,如果是三个参数就应该直接返回结果,但如果不足3个参数应该返回一个新函数,而返回新函数又有addCurry(1)(2, 3)与addCurry(1)(2)(3)两种形式,对于这种不确定调用几次的,内部一定得存在一个递归。那么尴尬的又来了,addCurry要返回新函数调用,那计算的结果谁来返回?所以这里一定得存在一个限制,它是跳出递归以及返回最终结果的核心因素。

const curry = function (fn, ...a) {
// 实参数量大于等于形参数量吗?
return a.length >= fn.length ?
// 如果大于返回执行结果
fn(...a) :
// 反之继续柯里化,递归,并将上一次的参数以及下次的参数继续传递下去
(...b) => curry(fn, ...a, ...b);
};
const add = (a, b, c) => a + b + c;
// 将add加工成柯里化函数
const addCurry = curry(add);
console.log(addCurry(1, 2, 3));// 6
console.log(addCurry(1)(2)(3));// 6
console.log(addCurry(1, 2)(3));// 6
console.log(addCurry(1)(2, 3));// 6

可能有同学初看这段代码有些不理解,这里我小白式解释下,我们以addCurry(1)(2)(3)调用为例:

  1. 初次调用curry(add),由于除了函数之外没别的参数,因此a长度是0,三元判断后addCurry就是(...b) => curry(fn, ...a, ...b)。
  2. 第一次调用addCurry(1),此时等同于(1) => curry(fn, [], [1]),注意,接下来神奇的事情发生了,function (fn, ...a)这一段中的...a直接把[]和[1]进行了合并,于是执行完毕继续递归时,下一次执行函数时的..a就是[1],即便函数执行完毕,自由变量依旧不会释放,这就是闭包的特性。
  3. 继续调用(2),那么此时就等同于(2) => curry(fn, [1], [2]),长度依旧不满足,继续返回递归,...a再次合并。
  4. 调用(3),此时等用于(3) => curry(fn, [1,2], [3]),...a再次合并,巧了,此时a.length >= fn.length满足条件,于是执行fn(...a),也就是add(1, 2, 3)。

同理,不管我们调用addCurry(1, 2, 3)还是addCurry(1,2)(3),都是相同的过程,实参长度大于等于形参长度吗?满足就返回执行结果,不满足就继续柯里化(递归),同时巧妙的把新旧参数进行合并。

伍 ❀ 实现无限调用

我们将问题再次升级,现在要求实现一个可以无限调用的函数,且每次调用都能得到最终结果,比如:

addCurry(1);
addCurry(1)(2);
addCurry(1)(2)(3, 4);
addCurry(1)(2)(3, 4)(5)(6, 7);

实现前,我们首先想到的是,由于没了形参数量的限制,此时就不可能存在在某种条件下跳出递归的条件了。但如果没条件,我们怎么知道什么时候返回函数,什么时候返回结果呢?

在说这个之前,我们先实现一个无限调用的函数,每次调用,它都会返回自己,且接受上次计算的结果,以及下次的参数,比如:

const add = (...a) => {
// 保留上一次的计算,同理也是最后一次的计算
let res = a.reduce((pre, cur) => pre + cur);
// 将上次的结果以及下次接受的参数都传下去
return (...b) => add(res, ...b);
};

现在尴尬的是,我们每次调用函数内部其实都做了求和,只是因为不断递归,我们拿不到结果,每次都是拿到一个新函数,怎么拿到结果?其实有一些做法,比如将结果绑在add上,或者借用toString方法,我们先实现:

const add = (...a) => {
let res = a.reduce((pre, cur) => pre + cur);
const add_ = (...b) => add(res, ...b);
// 因为每次返回的都是add_,因此要给它绑toString方法
add_.toString = () => {
return res;
};
return add_;
};
// 注意,方法前都有一个+
console.log(+add(1)(2));// 3
console.log(+add(1)(2, 3));// 6
console.log(+add(1)(2, 3)(4));// 10

我们用了一些奇技淫巧,在调用前添加了+,这样函数执行完毕后,因为+会自动调用我们定义的toString方法,从而返回了我们期望的结果。

在关于Object.prototype.toString()方法介绍中,我们可以得知:

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。

我们函数内部总是返回一个新函数,这也是为什么要将toString绑在新函数上的缘故,相当于我们覆盖了原型链上的toString方法,让它来帮我返回值,大概如此了。

陆 ❀ 总

那么到这里,我们从函数柯里化聊到了add(1)(2)(3),以此又拓展到了add(1, 2, 3)(4)以及无限调用的场景,本质上帮大家复习了一波闭包的小技巧。那么回到函数柯里化,花里胡哨说这么多,这东西有什么用呢?其实从传参上就能感觉到,它能做到参数缓存,没一次参数的传递,都会返回一个与该参数绑定的新函数。

我们假定add(1)(2)与add(1)(3)是两个场景,而add(1)这一步会进行非常复杂的计算,那么通过柯里化,我们能直接将add(1)这一步缓存起来,再以此拓展到不同的其它场景中,那这样是不是达到了复用的目的了呢?

关于函数柯里化以及这道题,就先说到这里了,假设以后运气好遇到了原题,那直接原地起飞,如果没遇到,我想通过本文,对于闭包的理解应该也有所加深,那么到这里本文结束。

从函数柯里化聊到add(1)(2)(3) add(1, 2)(3),以及柯里化无限调用的更多相关文章

  1. 无限调用函数add(1)(2)(3)......

    无限调用函数,并且累计结果 其实这也算一道面试题吧,笔者曾经被提问过,可惜当时没能答上来...

  2. 腾讯云图片鉴黄集成到C# SQL Server 怎么在分页获取数据的同时获取到总记录数 sqlserver 操作数据表语句模板 .NET MVC后台发送post请求 百度api查询多个地址的经纬度的问题 try{}里有一个 return 语句,那么紧跟在这个 try 后的 finally {}里的 code 会 不会被执行,什么时候被执行,在 return 前还是后? js获取某个日期

    腾讯云图片鉴黄集成到C#   官方文档:https://cloud.tencent.com/document/product/641/12422 请求官方API及签名的生成代码如下: public c ...

  3. try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,还是在return之后执行?

    这是一个很有趣的问题,我测试的结果是:是在return中间执行. 我在网上搜寻了一些资料,下面是参考代码: /** * */ package com.b510.test; /** * try {}里有 ...

  4. 在 VS 类库项目中 Add Service References 和 Add Web References 的区别

    原文:在 VS 类库项目中 Add Service References 和 Add Web References 的区别 出身问题: 1.在vs2005时代,Add Web Reference(添加 ...

  5. add jars、add external jars、add library、add class folder的区别

    add external jars = 增加工程外部的包add jars = 增加工程内包add library = 增加一个库add class folder = 增加一个类文件夹 add jar是 ...

  6. @有两个含义:1,在参数里,以表明该变量为伪参数 ,在本例中下文里将用@name变量代入当前代码中2,在字串中,@的意思就是后面的字串以它原本的含义显示,如果不

    @有两个含义:1,在参数里,以表明该变量为伪参数 ,在本例中下文里将用@name变量代入当前代码中 2,在字串中,@的意思就是后面的字串以它原本的含义显示,如果不加@那么需要用一些转义符\来显示一些特 ...

  7. git add -A 和 git add . 的区别

    git add -A和 git add .   git add -u在功能上看似很相近,但还是存在一点差别 git add . :他会监控工作区的状态树,使用它会把工作时的所有变化提交到暂存区,包括文 ...

  8. git add -A和git add . 的区别

    git add -A和 git add . git add -u在功能上看似很相近,但还是有所差别. git add . :他会监控工作区的状态树,使用它会把工作时的所有变化提交到暂存区,包括文件内容 ...

  9. jsp页面:js方法里嵌套java代码(是操作数据库的),如果这个js 方法没被调用,当jsp页面被解析的时候,不管这个js方法有没有被调用这段java代码都会被执行?

    jsp页面:js方法里嵌套java代码(是操作数据库的),如果这个js 方法没被调用,当jsp页面被解析的时候,不管这个js方法有没有被调用这段java代码都会被执行? 因为在解析时最新解析的就是JA ...

  10. Java异常处理中,try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后?

    Java异常处理中,try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后? 解答:会执行,在return前 ...

随机推荐

  1. java项目实践-webapp-mytomcat-day16

    目录 1. http协议 2. 自定义的web框架 3. 具体实现 4. 启动 1. http协议 CS架构 建立连接"三次握手" 断开连接 "四次挥手" 三次 ...

  2. mysql-字符函数-拼接-长度-切片-替换

  3. [转帖]linux设置page cache大小,Linux Page Cache调优在Kafka中的应用

    本文首发于 vivo互联网技术 微信公众号 链接: 作者:Yang Yijun 本文主要描述Linux Page Cache优化的背景.Page Cache的基本概念.列举之前针对Kafka的 IO ...

  4. [转帖]挂载文件系统选项nodiratime、noatime

    默认的方式下linux会把文件访问的时间atime做记录,文件系统在文件被访问.创建.修改等的时候记录下了文件的一些时间戳,比如:文件创建时间.最近一次修改时间和最近一次访问时间:这在绝大部分的场合都 ...

  5. [百度贴吧]部分CPU的SPEC2006int 结果

    这些测试成绩基本上是本人自己测试的结果.下表中有来自spec官网的两个成绩,因为测试年份较早,系统环境和编译器都较老,测试成绩本人实测的还差,所以仅作为参考.部分测试启用了自动并行和附加的优化库,是为 ...

  6. SQLSERVER 数据库根据LCK_M_S对应的waitsorce 查看被锁的表信息的简单方法

    公司一个开发大牛召冠总搞过一个 DMSQLMONITOR 工具 能够识别Oracle以及SQLSERVER 数据库的锁和事务等问题, 非常好用 今天环境出现了不可用的情况, 所以这边着急进行一下问题分 ...

  7. AI-WEB-1.0靶机

    AI-WEB-1.0靶机 情报收集 扫描靶机,打开网站提示 Not even Google search my contents! dirb http://192.168.218.139 扫描网站 进 ...

  8. CTT Day3

    T1 忘了叫什么名字 对于一个排列 \(p\),定义它的权值为其有多少个子串是一个值域从 \(1\) 开始的排列.给定排列 \(p\),对于 \(1\le i\le j\le n\),定义 \(f(i ...

  9. 浅浅的源码剖析grpc-go(一)

    最近在学习 rpc 相关的知识,如果让我去从头设计一个 rpc,我从使用者的角度出发,究竟需要去做一下什么工作? 第一,RPC 本质上就是一个远程调用,那肯定就需要通过网络来传输数据.虽然传输协议可以 ...

  10. 如何将数据库中的树结构表导出EXCEL

    我们常常会遇到这样的需求,将数据库中父子结构的表数据导出作为表头 我们实现的开发中就遇到这样的需求:将所有用户的权限表导出,权限作为表头 思路:sql求出每一条权限数据应该跨的行数,在代码中填充这些数 ...