前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >函数式编程中的数组问题

函数式编程中的数组问题

作者头像
Jean
发布2019-09-10 20:05:31
2K0
发布2019-09-10 20:05:31
举报
文章被收录于专栏:Web行业观察Web行业观察

这里只传授最高端的编程技巧...

好久没讲技术了,先回忆一下啥是函数式编程(FP)吧,比如FP要求使用表达式,不允许出现语句,这样更接近自然语言。



表达式取代经典语句

什么叫语句呢?学校编程课本上教的变量声明语句,循环语句,条件判断语句,枚举语句,这些都是语句,也就是说我们再熟悉不过的if/else语句,for/while循环,switch以及try/catch都不给用了!

没有这些语句还编个P程啊?我当时也有一种“这些年编程白学了”的冲动,虽然官方说每一种语句都可以用对应的表达式来替代,比如在JavaScript领域,变量声明省略掉关键词后就变成了表达式:

  • 变量声明语句
代码语言:javascript
复制
// 变量声明语句+赋值
let test = 123;

// 变量申明+赋值表达式
test = 123;

因为变量总是属于当前函数的变量对象(variable object),声明变量等同于给对象添加属性,所以变量申明表达式返回赋的值或者undefined。

  • if/else语句

函数式替换if/else语句也很简单,我们本来就有条件运算符(… ? … : …)可用:

代码语言:javascript
复制
// 条件语句
if(convention){}
else {}

// 条件表达式
convention ? expression1 : expression2;
  • switch语句

switch语句的话可以用js散列表来模拟,也就是对象:

代码语言:javascript
复制
// 状态枚举语句
switch (expression) {
  case value1:
    break;
  case value2:
    break;
  default:
    break;
}

// 字典表达式
({
  value1(){},
  value2(){},
})[expression] || default();
  • try&catch语句

至于try/catch/finally可以将同步流包裹进promise,再给他监听一个catch方法:

代码语言:javascript
复制
// 异常处理语句
try{
  // 代码块
}catch(err){
  
}finally{}

// 异常处理表达式
new Promise((res,rej)=>{
  // 代码块
}).catch(err=>{
  
}).finally(()=>{})

以上这些表达式都完美替换了经典语句,但是我在“如何取代循环语句”问题上思考了很久,循环语句不同于上面几种,循环问题是最复杂的,光语句语法就有for和while等好几种,如何取代这些傻吊语句成了一个问题。下面我来一一讨论一下,表达式是否能够完美的替换循环语句。

数组问题

Array对象(数组或者叫列表)是JavaScript里最重要的一个类,也是原型链上方法最多的一个。事实上JS里一切对象都是(散)列表。首先,所有循环都要使用数组,因为数组的长度(n)是衡量循环的时间复杂度的标准,通常循环一遍的复杂度就是O(n)。

  • 循环遍历

我们最常见的循环就是遍历一个数组,那直接可以利用数组的forEach方法来遍历:

代码语言:javascript
复制
// 遍历数组语句
for(let i=0; i<list.length; i++){
}

// 遍历数组方法
list.forEach(item=>{
})
  • 指定循环次数

for循环语句中经常出现需要指定循环的次数而没有数组,我们可以通过构造一个定长数组来遍历:

代码语言:javascript
复制
// 指定次数循环语句
for(let i=0; i<n; i++){
}

// 指定次数循环表达式
Array(n).fill(true).forEach(()=>{
})
  • continue中断本次迭代

continue关键词的作用是提前结束本次迭代进程,赶紧进入下一次迭代。在函数式数组的遍历中只要使用return结束当前回调的执行就行啦。

代码语言:javascript
复制
// continue语句
while (expression) {
   if (condition) {
      continue;
   }
}

// 用return结束当前迭代函数
list.forEach(()=>{
  if (condition) {
      return;
   }
})
  • break结束循环

和continue不同,break关键词会结束整个循环,forEach传的回调函数永远会执行列表的长度遍,所以forEach没用,同理map和filter等一系列数组遍历方法都不能用。可喜的是,数组有一些“可中断的遍历方法”,比如find方法本意是寻找一个数组元素,找到后就可以中断遍历;比如some方法本意是是否有“一些”元素符合回调条件,遍历时一旦匹配到一个就会停止向下匹配;比如every方法本意是是否“所有”元素都符合回调条件,遍历时只要发现1个元素不符合就会停止向下匹配。所以函数式编程中有3个数组方法可以实现循环的break。

代码语言:javascript
复制
// 传统break语句
for(let item of list){
  if(condition)break;
}

// 函数式break
// find
list.find(item=>{
  if(condition)return true;
})
// some
list.some(item=>{
  if(condition)return true;
})
// every
list.some(item=>{
  if(condition)return false;
})
  • 无限循环

取代无限循环语句只要递归调用自己就好啦~

代码语言:javascript
复制
// 无限循环语句
while(true){}

// 无限循环表达式
(function loop(){
  loop();
})();
  • 异步循环(划重点)

异步循环是最难的模拟的一个。假如我们有一个异步任务列表asyncTasks,想要串行执行而不是并行执行,也就是一个接着一个运行,如果想要并行执行任务非常简单,只要Promise.all(asyncTasks)就行了,但能不能实现一个Promise.sequential呢?如果任务数量确定可以直接.then().then()...来链式调用,但如果数量是动态的就得用循环了。首先模拟一个tasks列表,其中每个元素都是async函数,即返回promise的函数:

代码语言:javascript
复制
tasks = [2000, 1000, 3000].map(time => async () => {
    await new Promise(res => setTimeout(res, time));
    console.log(time);
})

使用循环语句来顺序执行非常舒适,但如果你尝试使用forEach来遍历就会出现问题:

代码语言:javascript
复制
// 异步链用循环语句+await非常合适
for(task of tasks){
  await task();
}

// 但是这样你会发现,若干个异步任务并发执行了!
tasks.forEach(async (task)=>{
  await task();
})

使用forEach,回调函数虽然是异步的,但是这个回调函数在一瞬间被并发执行了n次,每一次之间没有等待,导致串行失败。追根揭底,forEach无法顺序执行异步任务的原因是,回调函数每次执行完全独立,没有关联。贯穿Array原型链上几十种遍历方法中,似乎只有reduce和sort等寥寥几个方法可以实现前后关联。我们来模拟一个吧,利用reduce来polyfill一个Promise.sequential方法。

代码语言:javascript
复制
Promise.concurrent = Promise.all;
Promise.sequential = tasks => tasks.reduce(async (chain, nextTask) => {
    await chain;
    return nextTask();
}, Promise.resolve());
代码语言:javascript
复制
Promise.sequential(tasks)
  .then(()=>console.log('finished'));
// 依次打印2000,1000,3000,'finished'

老衲的解释:这里利用reduce将一系列promise串了起来,合成了一个大的promise,本质上仍然是通过.then将一个个promise链起来。注意,在async函数中即使return了一个promise.resolve(123),函数返回值将是另一个promise,只是解析值都是123。

经过本文的分析,所有的JavaScript语句,无论是声明,条件,枚举,循环还是流程控制语句,统统可以用函数表达式来替换,让JS成为第一个只由表达式组成的通用编程语言。如果认为我有遗漏的地方或者说还有哪些语句是不可取代的,欢迎在底下留言评论。

参考

  • https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/
  • https://stackoverflow.com/questions/24586110/resolve-promises-one-after-another-i-e-in-sequence
  • https://jakearchibald.com/2017/await-vs-return-vs-return-await/
  • https://jimmy.blog.csdn.net/article/details/91038735

(完)

【日记】

看看本文的参考链接,可以发现外网站点都习惯于将文章的标题放在url上作为文章ID,这种习惯的好处就是可以从url上直接读出内容的主题,而我们的站点url很多都是一个个文章编号。不得不说,这些专业论坛的文章的不仅质量高,url的设计也很有语义。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebHub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档