当前位置: 首页 > 科技观察

LooplessJavaScript

时间:2023-03-16 12:43:56 科技观察

我们的目标是编写低复杂度的JavaScript代码。通过选择一个合适的抽象来解决这个问题,但是你怎么知道选择哪个抽象呢?不幸的是,到目前为止,还没有找到具体的例子来回答这个问题。在本文中,我们讨论了如何在没有任何循环的情况下处理JavaScript数组,最终效果是降低代码复杂性。循环是一种重要的控制结构,难以重用和插入其他操作。另外,这意味着代码在每次迭代中不断变化。——LuisAtencio1.循环我们前面说过,循环这样的控制结构会引入复杂性。但是这并没有确切的证据,让我们先看看循环在JavaScript中是如何工作的。在JavaScript中,至少有四五种实现循环的方法,其中最基本的就是while循环。让我们首先创建一个示例函数和数组://oodlify::String->Stringfunctionoodlify(s){returns.replace(/[aeiou]/g,'oodle');}constinput=['John','Paul','乔治','林戈',];现在我们有了一个数组,我们想要对每个元素进行oodlify。如果使用while循环,则类似于:leti=0;constlen=input.length;letoutput=[];while(istrongest.strength){strongest=hero;}}糟透了。观看这个循环,每次都保存迄今为止最好的英雄。继续需求,接下来我们想要所有超级英雄的总力量:letcombinedStrength=0;for(heroofheroes){combinedStrength+=hero.strength;}在这两个例子中,一个变量在循环开始之前被初始化。然后在每个循环中,处理一个数组元素并更新这个变量。为了使这种循环模式更加明显,让我们将数组的中间部分提取到一个函数中。并重命名这些变量以进一步突出相似性。functiongreaterStrength(champion,contender){return(contender.strength>champion.strength)?contender:champion;}functionaddStrength(tally,hero){returntally+hero.strength;}constinitialStrongest={strength:0};(heroofheroes){working=greaterStrength(working,hero);}conststrongest=working;constinitialCombinedStrength=0;working=initialCombinedStrength;for(heroofheroes){working=addStrength(working,hero);}constcombinedStrength=working;写,两个循环变得很相似。它们两者之间的唯一区别是调用的函数和初始值。两者的作用都是对数组进行处理,最后得到一个值。因此,我们创建了一个reduce函数来封装这个模式。functionreduce(f,initialVal,a){letworking=initialVal;for(itemofa){working=f(working,item);}reduce模式在JavaScript中也很常用,所以JavaScript提供了数组的内置方法,你不用需要自己写。使用内置方法,代码变为:conststrongestHero=heroes.reduce(greaterStrength,{strength:0});constcombinedStrength=heroes.reduce(addStrength,0);ok,如果你足够细心,你会注意到上面的代码实际上并没有那么短。但是确实比自己写的reduce代码少写了几行。但我们的目标不是让代码更短或写得更少,而是降低代码的复杂度。现在复杂度降低了吗?我会说是的。将处理每个元素的代码和处理循环的代码分开,这样代码就不会相互纠缠,降低复杂度。乍一看,reduce方法似乎非常基础。我们提到的reduce也大多是加法等简单的例子。但是没有人说reduce方法只能返回原始类型,它可以是一个对象类型,甚至是另一个数组。第一次意识到这个问题的时候,我也是豁然开朗。所以其实你可以使用reduce方法来实现map或者filter,留给读者作为练习。四、过滤现在我们有map来处理数组中的每一个元素,reduce可以处理数组,最后得到一个值。但是如果你想获取数组中的一些元素怎么办?让我们进一步探索,现在给上面的超级英雄数组添加一些属性:constheroes=[{name:'Hulk',strength:90000,sex:'m'},{name:'Spider-Man',strength:25000,sex:'m'},{name:'HawkEye',strength:136,sex:'m'},{name:'Thor',strength:100000,sex:'m'},{name:'BlackWidow',strength:136,sex:'f'},{name:'Vision',strength:5000,sex:'m'},{name:'ScarletWitch',strength:60,sex:'f'},{name:'Mystique',strength:120,sex:'f'},{name:'Namora',strength:75000,sex:'f'},];好了,现在有两个问题,我们要:找到所有的女英雄;查找所有能量值大于500的英雄。使用普通的for...of循环,您将得到以下代码:letfemaleHeroes=[];for(letheroofheroes){if(hero.sex==='f'){femaleHeroes.push(hero);}}letsuperhumans=[];for(letheroofheroes){if(hero.strength>=500){superhumans.push(hero);}}逻辑严密,看起来不错?但是里面有重复。其实区别就在于if的判断语句,那么if语句是否可以重构为一个函数呢?functionisFemaleHero(英雄){return(hero.sex==='f');}functionisSuperhuman(英雄){return(hero.strength>=500);}letfemaleHeroes=[];for(letheroofheroes){if(isFemaleHero(英雄)){femaleHeroes.push(hero);}}letsuperhumans=[];for(letheroofheroes){if(isSuperhuman(hero)){superhumans.push(hero);}}这种只返回true或false的函数就是通常称为谓词函数。这里使用谓词函数来判断当前英雄是否需要保留。上面代码的写法会显得长一些,但是提取断言函数可以让重复的循环代码更加明显。现在将循环提取到一个函数中。functionfilter(predicate,arr){letworking=[];for(letitemofarr){if(predicate(item)){workingworking=working.concat(item);}}}constfemaleHeroes=filter(isFemaleHero,heroes);constsuperhumans=filter(是超人,英雄);和map、reduce一样,JavaScript提供了内置的数组方法,所以没必要自己去实现(除非你想自己写)。使用内置数组的方法,上面的代码就变成了:constfemaleHeroes=heroes.filter(isFemaleHero);constsuperhumans=heroes.filter(isSuperhuman);为什么这段代码比for...of循环更好?回想一下整个过程,我们要解决一个“找到所有满足某个条件的英雄”。使用过滤器可以简化问题。我们需要做的就是通过编写一个简单的函数来告诉过滤器要保留哪些数组元素。不需要考虑数组长什么样,繁琐的中间变量。取而代之的是一个简单的断言函数,仅此而已。与其他迭代函数相比,使用过滤器是一个双向过程。我们不需要通读循环代码来准确理解要过滤什么,要过滤的东西在传递给它的函数中。五、查找过滤器已经唾手可得。如果此时只想找一个英雄怎么办?例如,找到“黑寡妇”。使用filter会这样写:functionisBlackWidow(hero){return(hero.name==='BlackWidow');}constblackWidow=heroes.filter(isBlackWidow)[0];这段代码的问题是效率不够。过滤器会检查数组中的每一个元素,我们知道里面只有一个“黑寡妇”,找到她就可以停下来,不用看后面的元素。然后,仍然使用断言函数,我们编写一个查找函数来返回第一个匹配项的元素。functionfind(predicate,arr){for(letitemofarr){if(predicate(item)){returnitem;}}}constblackWidow=find(isBlackWidow,heroes);同样,JavaScript已经提供了这样一个方法:constblackWidow=heroes.find(isBlackWidow);find再次体现了四两表盘的特点。通过find方法,问题被简化为:你只需要关注如何判断你要找的是什么,而不用关心迭代如何实现等细节。6.总结这些迭代函数的例子很好地说明了“抽象”的作用和优雅。回想一下我们讲的内置方法,我们在每个例子中都做了三件事:去掉循环结构,让代码简洁易读;通过合适的方法名描述我们使用的模式,即:map、reduce、filter、find;将问题从处理整个数组简化为处理每个元素。请注意,在每种情况下,我们都会用几个纯函数分解和解决问题。真正令人兴奋的是,仅仅使用这四种模式(当然还有其他模式,我建议你学习它们),你可以消除JS代码中几乎所有的循环。这是因为在JS中几乎每一个循环都是用来处理数组,或者生成数组。通过消除循环,降低了复杂性并且代码更易于维护。点击《无循环 JavaScript》阅读原文。【本文为专栏作者“虎子打哈”原创文章,转载请联系作者获得授权】点此阅读更多该作者好文