少侠留步! 你可能一直都误解了map,filter和reduce

少侠们好~
今天和大家分享一篇关于JS数组里面的map,filter,reduce相关函数的知识,
相信少侠们肯定或多或少都了解过一些相关知识,特别对于map和filter这种很常用的函数的用法,可能已经信手拈来了,
但是~
少侠,
你真的确定,你目前所使用的方式,就是它们的正确使用姿势吗?
如果想知道所作的选择是否明智,就看你在清楚知道将会付出什么代价的情况下,是否依然选择那样做。
————《黑客帝国3》
首先,让我们从最一个比较简单的例子开始:
这里有一个从1到10的数组:

现在,如果少侠你需要取出中间的奇数,你会怎么做呢?
这个问题不难,有的少侠可能会使用for循环来过滤出偶数,某些少侠可能会使用while循环,又或是使用数组自带的filter方法。
这里,我们先不考虑使用for和while这些迭代方法,我们选择使用数组自带的filter方法,如果使用filter方法的话,
少侠你的代码大概可能会是这样的:

还是挺简单的对吧?
相信大部分少侠都能很轻松完成。
在filter函数内部,我们使用 num % 2 !== 0 来判断 是否是一个奇数,如果是一个奇数,就过滤出来。
在这里,有的少侠可能就会想到,如果把这个判断过程单独分成一个函数,会方便一些,也方便以后的修改扩展。
比如,单独写成一个 isOdd函数:

然后在filter里面使用这个函数,是吧?
但是!
一些少侠这里就会开始犯一个天真的错误了,
你可能会写成下面这样:

看见这样的代码,有的少侠可能会说,天辰,这样写没有什么问题啊,我也测试过了,结果都是正确的,怎么就天真了?
没错,结果是正确的,
但是,你每次在filter里面都额外调用了一次并不一定需要的函数。
也就是包裹着 return isOdd(num)的那个外层箭头函数。

如果你还没看出来问题的话,先看看这个例子:

这里的sayHello接受一个字符串name, 然后返回 'hello' + name.
helpSayHello同样接受一个字符串name,然后用这个name 调用 sayHello, 得到返回的 'hello' + name.
那么,少侠你看看,下面两句代码结果有什么区别呢?
我们干嘛不直接调用sayHello呢?

“这样啊,大概理解了,但是天辰你告诉我,鸽子为什么那么大? 到底哪里调用了2次了?”

“。。。。。。”
好吧,那换个角度,

现在发现了没?
filter里面那个函数是不是和isOdd一模一样?
所以,我们直接把isOdd放进去就可以了:

“道理我懂了,但是谁能告诉我,鸽子到底为什么那么大? 为什么之前那样会多调用一次函数?”
“。。。。。。。算了,接着看下面的内容吧”
如果要把nums里面的数字翻倍,用map函数,又该怎么做呢?
聪明的少侠这次肯定一下就明白了:

好了,既然现在我们知道了如何从nums数组里面过滤出奇数,如何翻倍里面的数字,那么,接下来我们就更进一步:
从nums里面过滤出所有奇数,并翻倍这些奇数。
这个问题也不难是吧?
让我猜一下,一些少侠应该会写成下面这样:

如果我们还想继续算出所有奇数翻倍后的和的话:

嗯,一个串一个看起来很酷是吧? 最后的结果也是正确的。
但是。。。
少侠你依然比较天真!
这样串起来的后果就是,我们会遍历3次数组,filter的时候遍历一次,map的时候遍历一次,reduce的时候再遍历一次。
现在数组只有10个数字,影响还不大,但是如果是很大的数组,比如100万个数字,遍历3遍的话,那么就会多访问150万次,
150万次什么概念?
少侠你有150万存款嘛?
更坏的情况会是当你习惯这种方式后,你可能会连接更多的函数。。。。
array
.map(fn1)
.map(fn2)
.filter(fn4)
.filter(fn5)
.forEach(fn6)
.....
所以,这种方式其实不算是一种很好的方式,
到这里有的少侠可能要问了,如果这种方式不是很好,有什么更好的方式呢? 我要怎么去找到更好的方式呢?
实际上,
要找到更好的方式,这里我们首先可以尝试的是
凭感觉。。。。
想象一下,如果让你自己从中间取出所有奇数,翻倍,并求和,你会觉得怎么做简单一些呢?
是不是觉得下面这样才比较正常?
首先,从第一个数字开始,如果它是奇数,就拿出来,翻倍,然后找个地方放起来,
接着,
继续看第二个数字,如果是偶数,就跳过,如果是奇数,就拿出来,翻倍,然后和开始的翻倍后的奇数加起来,找个地方放起来。
接着看第三个数字,以此类推,只要是奇数,就翻倍,然后和之前的结果加起来。
直到我们找寻到最后一个数字,就可以算出最终结果了,
对吧?我们并不需要再回头看了。
所以,这样才是正确的方式。
如果使用迭代的话,就是下面这样:

没错,通常这样才是正常的操作。
但是,
这样的代码不太方便,
少侠你肯定也不想每次都写这么一大堆for循环语句吧?
所以,我们可以先试着继续优化一下。
我们首先试着用一个叫做magic(魔法)的函数把这些步骤包裹起来:

现在看起来好多了,不过,magic内部的操作看起来不是很清晰,
其他少侠看见了可能很难一眼就明白magic函数在做什么,
我们可以试着把里面的一些步骤分成更小的函数:

去掉注释,换个稍微简洁点的写法:

现在比较简洁也比较清晰了,不过,假如我们现在要计算是偶数而不是奇数怎么办呢?
或者如果更复杂些,如果是偶数,就反过来,除以二,然后求和,又该怎么办呢?
难道我们又要复制一次magic函数,然后改动for循环里面的逻辑吗?
也许,我们可以把for循环内部的逻辑单独提取成一个外部函数!

但是,
少侠们注意了!
这里的问题是,在内部的时候,for可以通过闭包规则,访问到array, 访问到array[i],以及sum,
而当我们把magicFriend函数放在外面之后,它没办法再访问到magic内部的array, array[i],以及sum了,
因为之前所有逻辑都在magic函数内部,可以通过闭包规则访问到所需变量,现在不行了。
那么怎么解决呢?
很简单~
这里我们真正关心的是,如何通过其他方式获取到之前通过闭包获取到的参数。
既然闭包的规则没办法帮助我们获取到所需参数,我们就换一个能帮我们获取到对应参数的规则,这里我们就换成通过函数参数来获取:

当然,现在在magic内部,for循环里面严重依赖当前作用域中一个叫做magicFriend的函数,如果当前作用域中的magicFriend函数不见了,magic函数就没办法正常运行了,
为了更灵活,我们可以把magicFriend也换成通过参数传递给magic。

等一等!
还有个问题。。。
万一我们希望sum从100开始相加呢?
算了,那就把sum也提取出来,通过参数传进去吧。。。

到这里了,有些少侠可能要问了!
天辰你到底有完没完了?
整这些花里胡哨的有什么用?
别急! 少侠~ 接下来就是见证奇迹的时刻!
首先,我们给代码里面部分函数和参数改一下名字:

是不是有点眼熟了? 接着,我们换成数组自带的reduce函数:

最后回到我们上面的问题:

完全OK!
这次我们用reduce只遍历了一次数组就完成了之前的操作!
现在知道为什么之前要把reduce叫做magic(魔法)了吧!
不过~~
到这里, 有的少侠可能会觉得有点奇怪,那么之前的filter和map呢? 为什么现在就不需要它们了呢?
针对这个问题,天辰我的答案是:
因为我们根本就不是在单纯地执行map或filter操作~
如果少侠你真的只是单纯地想把一堆数据,映射成另一对数据,才是用map。
如果少侠你真的只是单纯地想过滤出一些数据,才是用filter。
除此之外,如果你既要过滤(filter)一些东西,同时又要映射(map)一些东西,
那么,少侠你实际上既不是在做map操作,也不是做filter操作,这个时候,请考虑采用reduce(magic)!
不行! 说好了只使用map,多一个filter,多一个map,都不算map!

好了,
恭喜你,少侠!
你成功发现并阅读完了这篇文章~
首先~
谢谢少侠你看到了这里,
然后~
不管少侠你现在处于什么阶段,希望你都能从中有所收获。
那么现在要结束了?
当然不行!
不管你们下次还有没有兴趣,按照惯例,在结束之前必须留个悬念吊一下胃口~
所以,提前预告一下下期内容。。。
下一次,天辰会和少侠们分享一个有趣的武器,
这个武器是天辰费了很大劲从救赎(JS)大陆获取到的,能够更酷(更装逼)地处理我们今天遇到的问题,
虽然可能需要少侠你有一定的修为才能使用,
但是~
不管怎样,我相信少侠你最终肯定能掌握它。

一些你可能关心的问题:
1、天辰,你说的这些,能帮助我涨工资吗?
说能的话显得有点装逼!但是,少侠如果你能像这样坚持不断学习的话,相信你以后不仅能涨工资,还能给别人发工资!
2、天辰,我不关心工资,我关心的是,了解这些能帮助我找到女朋友吗?
看见这个狗头了没??
我要有找女朋友的方法我还会在这写技术文章?!
当然,不排除某些优秀的少侠能在开发妹子面前用技术装逼从而吸引到妹子。。。
3、还有!天辰你的代码为什么要放图片,而不是直接贴文字代码呢?我都不能复制粘贴测试。
第一,放图片应该要好看些。
第二,不能复制代码就对了!少侠你应该自己动手敲一遍代码,这样印象才比较深刻,请养成自己动手的好习惯!
好了,
少侠,江湖路上,有缘再见~
