没有循环的JavaScript

有些文章中提到过,缩进(并不能特别准确的)说明了代码的复杂程度。我们想要的是简单的JavaScript。之所以层层缩进,是因为我们用抽象的方式解决问题。但要选用什么抽象方法呢?截止目前,我们没有在特定环境中说明该使用什么样的方法。本文将关注如何在摆脱循环的情况下使用数组。最终的结果当然是更简单可读的代码。

 

“……循环是个不可避免的结构,而且不好复用,同时循环还很难加入其他操作中。更麻烦的是,使用循环就意味着在每一个新的迭代中有更多变化需要响应”——Luis Atencio

 

循环

 

类似循环一样的控制结构会让代码变得复杂。但目前并没有什么证据能证明它。现在让我们看看JavaScript中的循环是如何工作的。

 

在JavaScript中,我们至少有4到5种循环的方法。最基本的要数while循环。开始之前我们先写一个示例函数和数组方便我们说明:

 

现在我们有了一个数组,来用oodify处理它。当我们使用while时,循环应该这样写:

 

请注意,为了知道我们所在的位置,我用了计数器i。首先将计数器初始化清零,之后在每次循环中加1计数。同时,我们还要比较i和数组长度,这样才能知道什么时候停止循环。JavaScript提供另外一个和它差不多,而且更简单的写法:for循环。用for循环可以这样写:

 

for循环是个很有用的结构,因为它可以将计数器的逻辑都包含在顶部,这是个很不错的改进。当我们使用while循环时,很容易就忘了写i的加1计数,然后造成无限循环。现在我们来看看这段代码的作用。我们试图对数组中的每个元素调用oodlify(),然后将结果存入新的数组。事实上我们不太想自己操作计数器。

 

这种对每一个数组元素做处理的方式十分常见。所以在ES2015中,提供了一个可以不用在意计数器的新的循环结构:for…of循环。每一轮循环它都会将数组中的下一个元素传给你。它看上去是这样的:

 

这个方法看上去简单很多。可以注意到计数器以及比较数组长度的过程都不见了。我们甚至不用自己将元素从数组中取出。for…of循环干了所有脏活累活。如果我们用for…of循环替代所有的for循环,就会取得很大进步。现在我们已经让代码变得更简单,但我们的目标不止如此。

 

映射(Mapping)

 

for…of循环要比for循环简单的多,但还是需要一些手动配置。首先需要初始化output数组,还要在每层循环中调用push()函数。如果能解决代码中一些现有的问题,还可以将代码变得更清晰明了,其存在的问题是:

如果有两个数组都需要调用oodlify怎么办?

 

首先想到的应该是对两个数组都用循环:

 

这固然有用。而且好处大于坏处。但这个方法重复使用了太多次——不是特别清爽。现在我们要去除一些重复来将它重构,先写一个函数:

 

看上去是不是好些了,但如果我们还有想要的函数怎么办?

 

这种情况下oodlifyArray()函数就帮不上什么忙了。如果我们创建一个izzlifyArrya()函数,这就又走了那个不断重复的老路。不管怎样,先试试,我们好看看到底是什么效果:

 

可以看出这两个函数功能惊人的相似。那如果我们可以将其中的模式抽象出来会怎样?事实上,我们想要的是:对于给出的数组和函数,将数组中的每个元素映射到新的数组中。然后把函数作用在每个元素上。我们把这种情况称为模式映射。数组的映射函数长这样:

 

这个方法还是没有完全摆脱循环。如果我们想要摆脱循环,那就需要写一个递归的版本:

 

递归的方法好像挺高级。只需要两行代码,没什么缩进。但一般来说,我们不怎么用递归的方法,因为它在老版本浏览器上性能不怎么样。而且事实上,我们并不需要自己去写映射函数(除非你想这么做)。映射函数实际上非常常见,所以JavaScript为我们提供了一个创建映射的方法。用映射方法,代码看上去是这样的:

 

注意到这种写法完全没有缩进。完全没有循环。事实上,其内部某些地方确实存在循环,但这完全不是我们需要关心的。这下代码看上去就非常的简单了。

 

那为什么这样写看上去特别简单呢?这个问题似乎特别蠢,但请仔细想想。是因为它特别短吗?答案是否定的。仅仅因为代码量少并不代表它简单。它看起来简单是因为我们把他们分开了。有两个函数来处理字符串:oofligy和izzlify。这些函数与数组或循环无关。另一个函数map会处理数组。但map不会管数组中的数据是什么类型,或你想用这些数据干什么。它只是在调用我们传给他的函数。和把所有东西混在一块不一样,我们将字符串的处理过程和数组的处理分开。这就是代码简单的原因。

 

精简(Reducing)

 

现在map这个函数非常便利,但它没办法覆盖我们需要的所有循环。它只在你想要创建一个和输入一样长的数组才有用。那如果我们想增加数组元素数量怎么办?或是想要在列表中找出最短的字符串。还有些时候我们想处理一个数组或将其元素减至一个。

 

现在来看个例子。假如我们有一个英雄对象的数组:

 

我们想找到最强壮的英雄。使用for循环,过程是这样:

 

代码看上去不错,它将所有事情考虑了进去。当我们开始循环的时候,始终可以从strongest中获取当前循环中最强壮的英雄。那新的问题来了,假设我们想知道所有英雄加起来有多强。

 

两个例子中,我们在开始循环之前都先初始化了一个变量。然后每一次循环中,都从数组中去取出一个值,然后更新这个变量。为了使循环更加简洁,我们从循环中提取因子然后使用函数。同时,我们将重新命名一些变量。

 

这样写的话,两个循环看上去就非常相近。两者的区别仅仅存在于函数名和初始值上。两个方法都将数组的元素减至一个。因而我们可以创建一个reduce函数来继续压缩这个模式。

 

JavaScript在reduce上为数组提供一种如map一样内嵌的方法。这样我们就不用自己写相关的方法了。使用内嵌的方法,代码应该是这样的:

 

如果大家有仔细阅读本文,你应该发现这段代码并不是最短的。使用数组内嵌的方法,我们也就减少了一行代码。但我们的目标是尽量减少函数的复杂性,而不是追求更少的代码量。那这样写到底有没有减少复杂性呢?答案是肯定的。将代码从独立处理元素的过程中分离,令其单独处理循环。这样代码就减少了一些复杂性。

 

过滤(Filtering)

 

我们首先使用map可以对数组中的每个元素进行操作。同时我们用reduce将数组减至一个元素。那如果我们想从数组中提取某些元素该怎么办?为了继续研究,来稍微扩充一下我们之前的数据:

 

现在面前有两个目标:

  • 找到所有女英雄;

  • 找到那些力量值大于500的英雄

 

先用老方法for循环,可以这样写:

 

这段代码挺不错的,它考虑到了所有的内容。但其中绝对有一些重复的模式。事实上,唯一改变的就是那个if条件申明。那现在将if拿出来单独作为函数。

 

这个的函数只会返回true或false,它有时被称为predicate。我们使用predicate来决定到底要不要保留heroes中的元素。

 

这样的写法让代码变得更长了。如果我们将predicate函数提取出来,提取后的版本变得清晰无比。我们用提取的部分创建函数。

 

和map,reduce一样,JavaScript为我们提供的也是数组方法。所以我们不用来自己多写什么(除非你想要这样做)。使用数组方法,代码变成了:

 

为什么这样比使用for循环强太多?思考一下我们是如何在例子中使用的。我们最初的问题是如何找出符合条件的英雄。当我们用filter函数解决了这个问题后,剩下的工作轻松无比。我们写了一个简单的函数,用它告诉filter函数哪些元素需要保留。最后我们写了一个非常简单的predicate函数,就不用考虑数组或变量了。

 

与其他方法相比,使用filter能传递更多信息,并且使用了更少的空间。完全没必要熟悉所有循环后来实现过滤。只需要写一个方法调用即可。

 

查找(Finding)

 

Filtering用起来很方便,但如果我们只想找一位英雄呢?假设我们要找到Black Widow。当使用filter函数:

 

这样写效率不高。filter需要查看数组中的每个元素。但我们知道只有一个Black Widow,完全可以在找到一个Black Widow后结束查找。predicate函数的用法是非常灵活的。我们可以来写一个find函数以返回匹配到的第一个元素。

 

和之前一样,JavaScript可以包办全部,我们不用自己创建什么复杂函数:

 

最终我们用较少的文字表达了更多内容。用find函数解决了之前查找特定元素的问题,现在有一个新的疑问:我怎么知道是找到特定的元素就结束还是遍历整个数组。然而这并不是我们要关心的内容!

 

小结:

 

从这些迭代函数中不难看出抽象思维的价值。假设我们用内嵌数组的方法处理一切问题。在每个案例中我们都完成了三件事:

  • 剔除循环控制结构,增强代码可读性;

  • 用现有的方法来归纳例子中的模式;

  • 明确我们到底要对数组中的元素做什么操作。

 

在每个例子中,我们都用小而纯粹的函数将问题分解。真正重要的就是这四种模式(也有其他方法,但我推荐这四种),用它们你几乎可以淘汰JavaScript中所有的循环了。这是因为几乎JavaScript中所有的循环都是来处理数组,或创建数组的。在减少循环的过程中,我们不但减少了代码的复杂性,同时也增强了代码的可维护性。

posted @ 2017-02-28 13:48  gj_kester  阅读(321)  评论(0编辑  收藏  举报