一个有味道的函数

简介: 最近想到了一个自认为很有意思的面试题如何实现一个compose函数。函数接收数个参数,参数均为Function类型,右侧函数的执行结果将作为左侧函数执行的参数来调用。 1 compose(arg => `${arg}%`, arg => arg.

最近想到了一个自认为很有意思的面试题
如何实现一个compose函数。
函数接收数个参数,参数均为Function类型,右侧函数的执行结果将作为左侧函数执行的参数来调用。

1 compose(arg => `${arg}%`, arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00%
2 compose(arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00
3 compose(arg => arg + 10)(5) // 15

 

执行结果如上述代码,有兴趣的同学可以先自己实现一下再来看后续的。

1.0实现方案

大致的思路为:

  1. 获取所有的参数
  2. 调用最后一个函数,并接收返回值
  3. 如果没有后续的函数,返回数据,如果有,将返回值放入下一个函数中执行

所以这种情况用递归来实现会比较清晰一些

 1 function compose (...funcs) {
 2   return function exec (arg) {
 3     let func = funcs.pop()
 4     let result = func(arg) // 执行函数,获取返回值
 5 
 6     // 如果后续还有函数,将返回值放入下一个函数执行
 7     // 如果后续没有了,直接返回
 8     return funcs.length ? exec(result) : result
 9   }
10 }

 

这样,我们就实现了上述的compose函数。
真是可喜可贺,可喜可贺。

本文完。




好了,如果现实生活中开发做需求也是如此爽快不做作就好了,但是,产品总是会来的,需求总是会改的。

2.0需求变更

我们现在有如下要求,函数需要支持Promise对象,而且要兼容普通函数的方式。
示例代码如下:

 1 // 为方便阅读修改的排版
 2 compose(
 3   arg => new Promise((resolve, reject) =>
 4     setTimeout(_ =>
 5       resolve(arg.toFixed(2)),
 6       1000
 7     )
 8   ),
 9   arg => arg + 10
10 )(5).then(data => {
11   console.log(data) // 15.00
12 })

 

我们有如下代码调用,对toFixed函数的调用添加1000ms的延迟。让用户觉得这个函数执行很慢,方便下次优化

所以,我们就需要去修改compose函数了。
我们之前的代码只能支持普通函数的处理,现在因为添加了Promise对象的原因,所以我们要进行如下修改:

首先,异步函数改为同步函数是不存在的readFile/readFileSync这类除外。
所以,最简单的方式就是,我们将普通函数改为异步函数,也就是在普通函数外包一层Promise

 1 function compose (...funcs) {
 2   return function exec (arg) {
 3     return new Promise((resolve, reject) => {
 4       let func = funcs.pop()
 5 
 6       let result = promiseify(func(arg)) // 执行函数,获取返回值,并将返回值转换为`Promise`对象
 7 
 8       // 注册`Promise`的`then`事件,并在里边进行下一次函数执行的准备
 9       // 判断后续是否还存在函数,如果有,继续执行
10       // 如果没有,直接返回结果
11       result.then(data => funcs.length ?
12         exec(data).then(resolve).catch(reject) :
13         resolve(data)
14       ).catch(reject)
15     })
16   }
17 }
18 
19 // 判断参数是否为`Promise`
20 function isPromise (pro) {
21   return pro instanceof Promise
22 }
23 
24 // 将参数转换为`Promise`
25 function promiseify (pro) {
26   // 如果结果为`Promise`,直接返回
27   if (isPromise(pro)) return pro
28   // 如果结果为这些基本类型,说明是普通函数
29   // 我们给他包一层`Promise.resolve`
30   if (['string', 'number', 'regexp', 'object'].includes(typeof pro)) return Promise.resolve(pro)
31 }

 

我们针对compose代码的改动主要是集中在这几处:

  1. compose的返回值改为了Promise对象,这个是必然的,因为内部可能会包含Promise参数,所以我们一定要返回一个Promise对象
  2. 将各个函数执行的返回值包装为了Promise对象,为了统一返回值。
  3. 处理函数返回值,监听thencatch、并将resolvereject传递了过去。

3.0终极版

现在,我们又得到了一个新的需求,我们想要在其中某些函数执行中跳过部分代码,先执行后续的函数,等到后续函数执行完后,再拿到返回值执行剩余的代码:

 1 compose(
 2   data => new Promise((resolve, reject) => resolve(data + 2.5)),
 3   data => new Promise((resolve, reject) => resolve(data + 2.5)),
 4   async function c (data, next) { // async/await为Promise语法糖,不赘述
 5     data += 10 // 数值 + 10
 6     let result = await next(data) // 先执行后续的代码
 7 
 8     result -= 5  // 数值 - 5
 9 
10     return result
11   },
12   (data, next) => new Promise((resolve, reject) => {
13     next(data).then(data => {
14       data = data / 100 // 将数值除以100限制百分比
15       resolve(`${data}%`)
16     }).catch(reject) // 先执行后续的代码
17   }),
18   function d (data) { return data + 20 }
19 )(15).then(console.log) // 0.45%

 

拿到需求后,陷入沉思。。。
好好地顺序执行代码,突然就变成了这个鸟样,随时可能会跳到后边的函数去。
所以我们分析这个新需求的效果:

我们在函数执行到一半时,执行了nextnext的返回值为后续函数的执行返回值。
也就是说,我们在next中处理,直接调用队列中的下一个函数即可;
然后监听thencatch回调,即可在当前函数中获取到返回值;
拿到返回值后就可以执行我们后续的代码。

然后他的实现呢,也是非常的简单,我们只需要修改如下代码即可完成操作:

1 // 在这里会强行调用`exec`并传入参数
2 // 而`exec`的执行,则意味着`funcs`集合中又一个函数被从队列中取出来
3 promiseify(func(arg, arg => exec(arg)))

 

也就是说,我们会提前执行下一个函数,而且下一个函数的then事件注册是在我们当前函数内部的,当我们拿到返回值后,就可以进行后续的处理了。
而我们所有的函数是存放在一个队列里的,在我们提前执行完毕该函数后,后续的执行也就不会再出现了。避免了一个函数被重复执行的问题。

如果看到这里已经很明白了,那么恭喜,你已经了解了实现koajs最核心的代码:
中间件的实现方式洋葱模型

想必现在整个函数周遭散发着洋葱的味道。

参考资料

koa-compose

相关示例代码仓库

1.0,普通函数
2.0,Promise函数
3.0,支持洋葱模型

目录
相关文章
|
算法 Linux C++
【Linux系统编程】深入解析Linux中read函数的错误场景
【Linux系统编程】深入解析Linux中read函数的错误场景
465 0
|
存储 NoSQL 安全
【C++调试】深入探索C++调试:从DWARF到堆栈解析
【C++调试】深入探索C++调试:从DWARF到堆栈解析
647 1
|
Web App开发 缓存 监控
Apollo插件:个性化你的开发流程
Apollo插件:个性化你的开发流程
|
9月前
|
数据挖掘 数据处理 索引
Pandas数据重命名:列名与索引为标题
Pandas 是强大的数据分析工具,支持灵活的数据结构和操作。本文介绍如何使用 Pandas 对 `DataFrame` 的列名和索引进行重命名,包括直接赋值法、`rename()` 方法及索引修改。通过代码示例展示了具体操作,并讨论了常见问题如名称冲突、数据类型不匹配及 `inplace` 参数的使用。掌握这些技巧可使数据更清晰易懂,便于后续分析。
539 29
|
9月前
|
安全 搜索推荐 测试技术
陪玩系统安全问题 陪玩系统用户体验 陪玩系统功能 陪玩搜索功能优化 陪玩系统开发教程
陪玩系统的安全问题至关重要,涉及用户数据保护、支付安全和平台稳定性。关键措施包括多因子认证、支付接口加密、防止恶意脚本注入、DDoS攻击防护及数据加密。同时,优化用户体验也非常重要,如简化操作流程、提供互动功能和个性化服务。核心功能涵盖用户注册、陪玩师资料展示、智能匹配、实时通讯、支付结算等。开发时需综合考虑需求分析、技术选型、界面设计和功能实现,并进行充分测试与优化,确保系统稳定性和安全性。
534 0
|
11月前
|
iOS开发 MacOS Windows
electron-updater实现electron全量版本更新
electron-updater实现electron全量版本更新
1881 9
electron-updater实现electron全量版本更新
|
12月前
|
人工智能 Cloud Native Serverless
2024云栖大会资料精选,《云原生+AI核心技术&最佳实践》PPT全量放送!
关注阿里云云原生公众号,后台回复:2024 云栖大会,即可免费下载云原生云栖大会核心资料合集。
2503 35
|
10月前
|
存储 NoSQL Linux
《探秘程序崩溃:核心转储(Core Dump)分析全攻略》
在软件开发中,程序崩溃如同暴风雨,核心转储(Core Dump)则是这场风暴后的“事故现场记录”。它保存了程序崩溃时的内存状态和寄存器信息,为开发者提供了关键线索,帮助快速定位问题根源,节省调试时间。通过设置如Linux的`ulimit -c unlimited`或Windows的WinDbg,可生成核心转储文件,并利用GDB等工具分析调用栈和内存信息,结合源代码,揭示崩溃原因,提升软件稳定性。
525 7
|
存储 安全 Java
缓冲区溢出之堆溢出(Heap Overflow)
【8月更文挑战第18天】
646 3
|
小程序 API
uni-app上传音频,图片步骤
uni-app上传音频,图片步骤
424 0