[译] 没有循环的 JavaScript

BAFNikole 7年前
   <p>有些文章中提到过,缩进(并不能特别准确的)说明了代码的复杂程度。我们想要的是简单的JavaScript。之所以层层缩进,是因为我们用抽象的方式解决问题。但要选用什么抽象方法呢?截止目前,我们没有在特定环境中说明该使用什么样的方法。本文将关注如何在摆脱循环的情况下使用数组。最终的结果当然是更简单可读的代码。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7ddbb0e8ab666172575a3618108295eb.png"></p>    <p><em>“……循环是个不可避免的结构,而且不好复用,同时循环还很难加入其他操作中。更麻烦的是,使用循环就意味着在每一个新的迭代中有更多变化需要响应”——Luis Atencio</em></p>    <h2>循环</h2>    <p>类似循环一样的控制结构会让代码变得复杂。但目前并没有什么证据能证明它。现在让我们看看JavaScript中的循环是如何工作的。</p>    <p>在JavaScript中,我们至少有4到5种循环的方法。最基本的要数 while 循环。开始之前我们先写一个示例函数和数组方便我们说明:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f34a9516bea41a5c52b1af9c6db6e512.png"></p>    <p>现在我们有了一个数组,来用 oodify 处理它。当我们使用 while 时,循环应该这样写:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d83ea37740e52feb219128448505e98b.png"></p>    <p>请注意,为了知道我们所在的位置,我用了计数器 i 。首先将计数器初始化清零,之后在每次循环中加1计数。同时,我们还要比较i和数组长度,这样才能知道什么时候停止循环。JavaScript提供另外一个和它差不多,而且更简单的写法: for 循环。用 for 循环可以这样写:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3083081115436cae38b04e60d462c85f.png"></p>    <p>for 循环是个很有用的结构,因为它可以将计数器的逻辑都包含在顶部,这是个很不错的改进。当我们使用 while 循环时,很容易就忘了写 i 的加1计数,然后造成无限循环。现在我们来看看这段代码的作用。我们试图对数组中的每个元素调用 oodlify() ,然后将结果存入新的数组。事实上我们不太想自己操作计数器。</p>    <p>这种对每一个数组元素做处理的方式十分常见。所以在ES2015中,提供了一个可以不用在意计数器的新的循环结构: for…of 循环。每一轮循环它都会将数组中的下一个元素传给你。它看上去是这样的:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fc4e69cc59f668df024fc1b8f40735de.png"></p>    <p>这个方法看上去简单很多。可以注意到计数器以及比较数组长度的过程都不见了。我们甚至不用自己将元素从数组中取出。 for…of 循环干了所有脏活累活。如果我们用 for…of 循环替代所有的for循环,就会取得很大进步。现在我们已经让代码变得更简单,但我们的目标不止如此。</p>    <h2>映射(Mapping)</h2>    <p>for…of 循环要比 for 循环简单的多,但还是需要一些手动配置。首先需要初始化 output 数组,还要在每层循环中调用 push() 函数。如果能解决代码中一些现有的问题,还可以将代码变得更清晰明了,其存在的问题是:</p>    <p>如果有两个数组都需要调用 oodlify 怎么办?</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/750b2e206385e3e230d8fe1d0eacda6f.png"></p>    <p>首先想到的应该是对两个数组都用循环:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4526f68e07c0f6f5c7f3eda70ffe3080.png"></p>    <p>这固然有用。而且好处大于坏处。但这个方法重复使用了太多次——不是特别清爽。现在我们要去除一些重复来将它重构,先写一个函数:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/77368a9c65dae395e2e75937cdc34aa7.png"></p>    <p>看上去是不是好些了,但如果我们还有想要的函数怎么办?</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7cfd5ba8a7b3040ced783ff4da3b1108.png"></p>    <p>这种情况下 oodlifyArray() 函数就帮不上什么忙了。如果我们创建一个 izzlifyArrya() 函数,这就又走了那个不断重复的老路。不管怎样,先试试,我们好看看到底是什么效果:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/82ca42619232ba703321a3355779d3db.png"></p>    <p>可以看出这两个函数功能惊人的相似。那如果我们可以将其中的模式抽象出来会怎样?事实上,我们想要的是:对于给出的数组和函数,将数组中的每个元素映射到新的数组中。然后把函数作用在每个元素上。我们把这种情况称为模式映射。数组的映射函数长这样:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e7ee5fdb8cc796753433c68251efdb5b.png"></p>    <p>这个方法还是没有完全摆脱循环。如果我们想要摆脱循环,那就需要写一个递归的版本:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fea0f48a774155a29adf7b3a73d96328.png"></p>    <p>递归的方法好像挺高级。只需要两行代码,没什么缩进。但一般来说,我们不怎么用递归的方法,因为它在老版本浏览器上性能不怎么样。而且事实上,我们并不需要自己去写映射函数(除非你想这么做)。映射函数实际上非常常见,所以JavaScript为我们提供了一个创建映射的方法。用映射方法,代码看上去是这样的:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e7c9062252a363ce12ea9da60106afc0.png"></p>    <p>注意到这种写法完全没有缩进。完全没有循环。事实上,其内部某些地方确实存在循环,但这完全不是我们需要关心的。这下代码看上去就非常的简单了。</p>    <p>那为什么这样写看上去特别简单呢?这个问题似乎特别蠢,但请仔细想想。是因为它特别短吗?答案是否定的。仅仅因为代码量少并不代表它简单。它看起来简单是因为我们把他们分开了。有两个函数来处理字符串: oofligy 和 izzlify 。这些函数与数组或循环无关。另一个函数 map 会处理数组。但 map 不会管数组中的数据是什么类型,或你想用这些数据干什么。它只是在调用我们传给他的函数。和把所有东西混在一块不一样,我们将字符串的处理过程和数组的处理分开。这就是代码简单的原因。</p>    <h2>精简(Reducing)</h2>    <p>现在 map 这个函数非常便利,但它没办法覆盖我们需要的所有循环。它只在你想要创建一个和输入一样长的数组才有用。那如果我们想增加数组元素数量怎么办?或是想要在列表中找出最短的字符串。还有些时候我们想处理一个数组或将其元素减至一个。</p>    <p>现在来看个例子。假如我们有一个英雄对象的数组:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/988109a01049c54802c5eda8aafa584e.png"></p>    <p>我们想找到最强壮的英雄。使用 for 循环,过程是这样:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/17c1705698159a248886d22fb92baacb.png"></p>    <p>代码看上去不错,它将所有事情考虑了进去。当我们开始循环的时候,始终可以从strongest中获取当前循环中最强壮的英雄。那新的问题来了,假设我们想知道所有英雄加起来有多强。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a72905d2a8fb2ff220f4490b3c3f7d30.png"></p>    <p>两个例子中,我们在开始循环之前都先初始化了一个变量。然后每一次循环中,都从数组中去取出一个值,然后更新这个变量。为了使循环更加简洁,我们从循环中提取因子然后使用函数。同时,我们将重新命名一些变量。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2e36d22e78e0f17f71bc0de270e3d6da.png"></p>    <p>这样写的话,两个循环看上去就非常相近。两者的区别仅仅存在于函数名和初始值上。两个方法都将数组的元素减至一个。因而我们可以创建一个 reduce 函数来继续压缩这个模式。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/aa825c5a186b062e9e8797f73235ddd3.png"></p>    <p>JavaScript在reduce上为数组提供一种如 map 一样内嵌的方法。这样我们就不用自己写相关的方法了。使用内嵌的方法,代码应该是这样的:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2a7e4f060b02de24d089af010a0089d7.png"></p>    <p>如果大家有仔细阅读本文,你应该发现这段代码并不是最短的。使用数组内嵌的方法,我们也就减少了一行代码。但我们的目标是尽量减少函数的复杂性,而不是追求更少的代码量。那这样写到底有没有减少复杂性呢?答案是肯定的。将代码从独立处理元素的过程中分离,令其单独处理循环。这样代码就减少了一些复杂性。</p>    <h2>过滤(Filtering)</h2>    <p>我们首先使用 map 可以对数组中的每个元素进行操作。同时我们用 reduce 将数组减至一个元素。那如果我们想从数组中提取某些元素该怎么办?为了继续研究,来稍微扩充一下我们之前的数据:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9cca8af75412a7aa0e8a931a975ae781.png"></p>    <p>现在面前有两个目标:</p>    <ol>     <li>找到所有女英雄;</li>     <li>找到那些力量值大于500的英雄</li>    </ol>    <p>先用老方法 for 循环,可以这样写:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/dd7ca155e621dc06a061770178903ad8.png"></p>    <p>这段代码挺不错的,它考虑到了所有的内容。但其中绝对有一些重复的模式。事实上,唯一改变的就是那个 if 条件申明。那现在将 if 拿出来单独作为函数。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/839d2b611ba44fb249fdd6c8beb002fa.png"></p>    <p>这个的函数只会返回 true 或 false ,它有时被称为 predicate 。我们使用 predicate 来决定到底要不要保留heroes中的元素。</p>    <p>这样的写法让代码变得更长了。如果我们将 predicate 函数提取出来,提取后的版本变得清晰无比。我们用提取的部分创建函数。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2e36d22e78e0f17f71bc0de270e3d6da.png"></p>    <p>和 map , reduce 一样,JavaScript为我们提供的也是数组方法。所以我们不用来自己多写什么(除非你想要这样做)。使用数组方法,代码变成了:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/98db3bed3f73a4b0d14ea7f97270566c.png"></p>    <p>为什么这样比使用 for 循环强太多?思考一下我们是如何在例子中使用的。我们最初的问题是如何找出符合条件的英雄。当我们用 filter 函数解决了这个问题后,剩下的工作轻松无比。我们写了一个简单的函数,用它告诉 filter 函数哪些元素需要保留。最后我们写了一个非常简单的 predicate 函数,就不用考虑数组或变量了。</p>    <p>与其他方法相比,使用 filter 能传递更多信息,并且使用了更少的空间。完全没必要熟悉所有循环后来实现过滤。只需要写一个方法调用即可。</p>    <h2>查找(Finding)</h2>    <p>Filtering用起来很方便,但如果我们只想找一位英雄呢?假设我们要找到Black Widow。当使用 filter 函数:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ed77a1e7e5d0115e311b0e6b5356f0e0.png"></p>    <p>这样写效率不高。 filter 需要查看数组中的每个元素。但我们知道只有一个Black Widow,完全可以在找到一个Black Widow后结束查找。 predicate 函数的用法是非常灵活的。我们可以来写一个 find 函数以返回匹配到的第一个元素。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3e4e0ac00861212574ed2f6225f12de5.png"></p>    <p>和之前一样,JavaScript可以包办全部,我们不用自己创建什么复杂函数:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6416b866e7719bbad0a15dfe3060b823.png"></p>    <p>最终我们用较少的文字表达了更多内容。用 find 函数解决了之前查找特定元素的问题,现在有一个新的疑问:我怎么知道是找到特定的元素就结束还是遍历整个数组。然而这并不是我们要关心的内容!</p>    <h2>小结:</h2>    <p>从这些迭代函数中不难看出抽象思维的价值。假设我们用内嵌数组的方法处理一切问题。在每个案例中我们都完成了三件事:</p>    <ol>     <li>剔除循环控制结构,增强代码可读性;</li>     <li>用现有的方法来归纳例子中的模式;</li>     <li>明确我们到底要对数组中的元素做什么操作。</li>    </ol>    <p>在每个例子中,我们都用小而纯粹的函数将问题分解。真正重要的就是这四种模式(也有其他方法,但我推荐这四种),用它们你几乎可以淘汰JavaScript中所有的循环了。这是因为几乎JavaScript中所有的循环都是来处理数组,或创建数组的。在减少循环的过程中,我们不但减少了代码的复杂性,同时也增强了代码的可维护性。</p>    <p> </p>    <p> </p>    <p>来自:https://github.com/Findow-team/Blog/issues/16</p>    <p> </p>