专栏首页Web行业观察函数式编程中的数组问题

函数式编程中的数组问题

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

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



表达式取代经典语句

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

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

  • 变量声明语句
// 变量声明语句+赋值
let test = 123;

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

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

  • if/else语句

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

// 条件语句
if(convention){}
else {}

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

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

// 状态枚举语句
switch (expression) {
  case value1:
    break;
  case value2:
    break;
  default:
    break;
}

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

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

// 异常处理语句
try{
  // 代码块
}catch(err){
  
}finally{}

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

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

数组问题

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

  • 循环遍历

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

// 遍历数组语句
for(let i=0; i<list.length; i++){
}

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

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

// 指定次数循环语句
for(let i=0; i<n; i++){
}

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

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

// 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。

// 传统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;
})
  • 无限循环

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

// 无限循环语句
while(true){}

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

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

tasks = [2000, 1000, 3000].map(time => async () => {
    await new Promise(res => setTimeout(res, time));
    console.log(time);
})

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

// 异步链用循环语句+await非常合适
for(task of tasks){
  await task();
}

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

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

Promise.concurrent = Promise.all;
Promise.sequential = tasks => tasks.reduce(async (chain, nextTask) => {
    await chain;
    return nextTask();
}, Promise.resolve());
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的设计也很有语义。

本文分享自微信公众号 - WebHub(myWebHub),作者:金恒昱

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 了解SSH加密和连接过程【官方推荐教程】

    SSH或安全shell是一种安全协议,是安全管理远程服务器的最常用方法。使用多种加密技术,SSH提供了一种机制,用于在双方之间建立加密安全连接,向另一方验证每一...

    Jean
  • typeof最新原理解析

    我们都知道 typeof(null) === 'object',关于原因,在小黄书《你不知道的JavaScript》中有这么一段解释:

    Jean
  • 工作记录 | 基于DocSearch黑一套搜索引擎

    记录一下最近工作中利用DocSearch,基于ServiceWorker和CacheAPI“恶搞”的一套Wiki搜索引擎,挺有意思的。

    Jean
  • 【编程基础】C++ Primer快速入门之八:语句

    1 定义: 语句就是我们说话,当然这里是指我们用计算机来说话--说人话。类似于自然语言中的句子。C++设计了简单的一句话语句,也设计了由一组语句组成的复杂语句-...

    程序员互动联盟
  • 【笔记】《C++Primer》—— 第5章:语句

    第五章的标题是语句,主要讲的是我们平时写语句的一些基本要求和例如迭代语句控制语句等概念。这篇内容比较少而且因为平时用得很多所以写的自然也会少些。

    ZifengHuang
  • 程序流程结构-选择结构

    3 种循环都可以使用 continue 语句。执行到该语句时,会跳过本次迭代的剩余部分,并开始下一轮 迭代。如果 continue 语句在嵌套循环内,则只会影响...

    mcxfate
  • PHP流程控制语句

    白胡杨同学
  • JavaScript之选择控制语句(if,switch,while,do-while,for循环)及很重要的表达式真与假

    在程序代码中,我们经常都会使用流程控制语句,它是用来控制程序中各语句执行顺序的语句,利用语句的组合便能完成一定功能的小逻辑模块   

    itclanCoder
  • Java面试题:小白不得不懂的斐波那契数列

    很长一段时间里,我都非常疑惑:“我写的技术文章不差啊,有内容的同时还很有趣,不至于每篇只有区区几十个人读啊?为什么有些内容简单到只有一行注册码的文章浏览量反而轻...

    沉默王二
  • python(六)

    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    py3study

扫码关注云+社区

领取腾讯云代金券