无循环 JavaScript(map、reduce、filter和find)

之前有讨论过,缩进(非常粗鲁地)增加了代码复杂性。我们的目标是写出复杂度低的 JavaScript 代码。通过选择一种合适的抽象来解决这个问题,可是你怎么能知道选择哪一种抽象呢?很遗憾的是到目前为止,没有找到一个具体的例子能回答这个问题。这篇文章中我们讨论不用任何循环如何处理 JavaScript 数组,最终得出的效果是可以降低代码复杂性。

循环是一种很重要的控制结构,它很难被重用,也很难插入到其他操作之中。另外,它意味着随着每次迭代,代码也在不断的变化之中。——Luis Atencio

我们先前说过,像循环这样的控制结构引入了复杂性。但是也没有给出确切的证据证明这一点,我们先看看 JavaScript 中循环的工作原理。

循环

在 JavaScript 中,至少有四、五种实现循环的方法,最基础的是 while 循环。我们首先先创建一个示例函数和数组:

// oodlify :: String -> String
function oodlify(s) {
 return s.replace(/[aeiou]/g, 'oodle');
}

const input = [
 'John',
 'Paul',
 'George',
 'Ringo',
];

现在有了一个数组,我们想要用 oodlify 函数处理每一个元素。如果用 while 循环,就类似于这样:

let i = 0;
const len = input.length;
let output = [];
while (i < len) {
 let item = input[i];
 let newItem = oodlify(item);
 output.push(newItem);
 i = i + 1;
}

注意这里发生的事情,我们用了一个初始值为 0 的计数器 i,每次循环都会自增。而且每次循环中都和 len 进行比较以保证循环特定次数以后终止循环。这种利用计数器进行循环控制的模式太常用了,所以 JavaScript 提供了一种更加简洁的写法: for 循环,写起来如下:

const len = input.length;
let output = [];
for (let i = 0; i < len; i = i + 1) {
 let item = input[i];
 let newItem = oodlify(item);
 output.push(newItem);
}

这一结构非常有用,while循环非常容易把自增的 i 给忘掉,进而引起无限循环;而for循环把和计数器相关的代码都放到了上面,这样你就不会忘掉自增 i,这确实是一个很好的改进。现在回到原来的问题,我们目标是在数组的每个元素上运行 oodlify() 函数,并且将结果放到一个新的数组中。

对一个数组中每个元素都进行操作的这种模式也是非常普遍的。因此在 ES2015 中,引入了一种新的循环结构可以把计数器也简化掉: for...of 循环。每一次返回数组的下一个元素给你,代码如下:

let output = [];
for (let item of input) {
 let newItem = oodlify(item);
 output.push(newItem);
}

这样就清晰很多了,注意这里计数器和比较都不用了,你甚至都不用把元素从数组里面取出来。for...of 帮我们做了里面的脏活累活。如果现在用 for...of 来代替所有的 for 循环,其实就可以很大程度上降低复杂性。但是,我们还可以做进一步的优化。

mapping

for...of 循环比 for 循环更清晰,但是依然需要一些配置性的代码。如不得不初始化一个 output 数组并且每次循环都要调用 push() 函数。但有办法可以让代码更加简洁有力,我们先扩展一下问题。

如果有两个数组需要调用 oodlify 函数会怎么样?

const fellowship = [
 'frodo',
 'sam',
 'gandalf',
 'aragorn',
 'boromir',
 'legolas',
 'gimli',
];

const band = [
 'John',
 'Paul',
 'George',
 'Ringo',
];

很容易想到的方法是对每个数组都做循环:

let bandoodle = [];
for (let item of band) {
 let newItem = oodlify(item);
 bandoodle.push(newItem);
}

let floodleship = [];
for (let item of fellowship) {
 let newItem = oodlify(item);
 floodleship.push(newItem);
}

这确实ok,有能正确执行的代码,就比没有好。但是重复的代码太多了——不够“DRY”。我们来重构它以降低重复性,创建一个函数:

function oodlifyArray(input) {
 let output = [];
 for (let item of input) {
  let newItem = oodlify(item);
  output.push(newItem);
 }
 return output;
}

let bandoodle = oodlifyArray(band);
let floodleship = oodlifyArray(fellowship);

这看起来好多了,可是如果我们想使用另外一个函数该怎么办?

function izzlify(s) {
 return s.replace(/[aeiou]+/g, 'izzle');
}

上面的 oodlifyArray() 一点用都没有了。但如果再创建一个 izzlifyArray() 函数的话,代码又重复了。不管那么多,先写出来看看什么效果:

function oodlifyArray(input) {
 let output = [];
 for (let item of input) {
  let newItem = oodlify(item);
  output.push(newItem);
 }
 return output;
}

function izzlifyArray(input) {
 let output = [];
 for (let item of input) {
  let newItem = izzlify(item);
  output.push(newItem);
 }
 return output;
}

这两个函数惊人的相似。那么是不是可以把它们抽象成一个通用的模式呢?我们想要的是:给定一个函数和一个数组,通过这个函数,把数组中的每一个元素做操作后放到新的数组中。我们把这个模式叫做 map 。一个数组的 map 函数如下:

function map(f, a) {
 let output = [];
 for (let item of a) {
  output.push(f(item));
 }
 return output;
}

这里还是用了循环结构,如果想要完全摆脱循环的话,可以做一个递归的版本出来:

function map(f, a) {
 if (a.length === 0) { return []; }
 return [f(a[0])].concat(map(f, a.slice(1)));
}

递归解决方法非常优雅,仅仅用了两行代码,几乎没有缩进。但是通常并不提倡于在这里使用递归,因为在较老的浏览器中的递归性能非常差。实际上,map 完全不需要你自己去手动实现(除非你自己想写)。map 模式很常用,因此 JavaScript 提供了一个内置 map 方法。使用这个 map 方法,上面的代码变成了这样:

let bandoodle  = band.map(oodlify);
let floodleship = fellowship.map(oodlify);
let bandizzle  = band.map(izzlify);
let fellowshizzle = fellowship.map(izzlify);

可以注意到,缩进消失,循环消失。当然循环可能转移到了其他地方,但是我们已经不需要去关心它们了。现在的代码简洁有力,完美。

为什么这个代码这么简单呢?这可能是个很傻的问题,不过也请思考一下。是因为短吗?不是,简洁并不代表不复杂。它的简单是因为我们把问题分离了。有两个处理字符串的函数: oodlify 和 izzlify,这些函数并不需要知道关于数组或者循环的任何事情。同时,有另外一个函数:map ,它来处理数组,它不需要知道数组中元素是什么类型的,甚至你想对数组做什么也不用关心。它只需要执行我们所传递的函数就可以了。把对数组的处理中和对字符串的处理分离开来,而不是把它们都混在一起。这就是为什么说上面的代码很简单。

reducing

现在,map 已经得心应手了,但是这并没有覆盖到每一种可能需要用到的循环。只有当你想创建一个和输入数组同样长度的数组时才有用。但是如果你想要向数组中增加几个元素呢?或者想找一个列表中的最短字符串是哪个?其实有时我们对数组进行处理,最终只想得到一个值而已。

来看一个例子,现在一个数组里面存放了一堆超级英雄:

const heroes = [
 {name: 'Hulk', strength: 90000},
 {name: 'Spider-Man', strength: 25000},
 {name: 'Hawk Eye', strength: 136},
 {name: 'Thor', strength: 100000},
 {name: 'Black Widow', strength: 136},
 {name: 'Vision', strength: 5000},
 {name: 'Scarlet Witch', strength: 60},
 {name: 'Mystique', strength: 120},
 {name: 'Namora', strength: 75000},
];

现在想找最强壮的超级英雄。使用 for...of 循环,像这样:

let strongest = {strength: 0};
for (hero of heroes) {
 if (hero.strength > strongest.strength) {
  strongest = hero;
 }
}

虽然这个代码可以正确运行,可是实在太烂了。看这个循环,每次都保存到目前为止最强的英雄。继续提需求,接下来我们想要所有超级英雄的总强度:

let combinedStrength = 0;
for (hero of heroes) {
 combinedStrength += hero.strength;
}

在这两个例子中,都在循环开始之前初始化了一个变量。然后在每一次的循环中,处理一个数组元素并且更新这个变量。为了使这种循环套路变得更加明显一点,现在把数组中间的部分抽离到一个函数当中。并且重命名这些变量,以进一步突出相似性。

function greaterStrength(champion, contender) {
 return (contender.strength > champion.strength) ? contender : champion;
}

function addStrength(tally, hero) {
 return tally + hero.strength;
}

const initialStrongest = {strength: 0};
let working = initialStrongest;
for (hero of heroes) {
 working = greaterStrength(working, hero);
}
const strongest = working;

const initialCombinedStrength = 0;
working = initialCombinedStrength;
for (hero of heroes) {
 working = addStrength(working, hero);
}
const combinedStrength = working;

用这种方式来写,两个循环变得非常相似了。它们两个之间唯一的区别是调用的函数和初始值不同。两个的功能都是对数组进行处理,最终得到一个值。所以,我们创建一个 reduce 函数来封装这个模式。

function reduce(f, initialVal, a) {
 let working = initialVal;
 for (item of a) {
  working = f(working, item);
 }
 return working;
}

reduce 模式在 JavaScript 中也是很常用的,因此 JavaScript 为数组提供了内置的方法,不需要自己来写。通过内置方法,代码就变成了:

const strongestHero = heroes.reduce(greaterStrength, {strength: 0});
const combinedStrength = heroes.reduce(addStrength, 0);

ok,如果足够细心的话,你会注意到上面的代码其实并没有短很多。不过也确实比自己手写的 reduce 代码少写了几行。但是我们的目标并不是使代码变短或者少写,而是降低代码复杂度。现在的复杂度降低了吗?我会说是的。把处理每个元素的代码和处理循环代码分离开来了,这样代码就不会互相纠缠在一起了,降低了复杂度。

reduce 方法乍一看可能觉得非常基础。我们举的 reduce 大部分也比如做加法这样的简单例子。但是没有人说 reduce 方法只能返回基本类型,它可以是一个 object 类型,甚至可以是另一个数组。当我第一次意识到这个问题的时候,自己也是豁然开朗。所以其实可以用 reduce 方法来实现 map 或者 filter,这个留给读者自己做练习。

filtering

现在我们有了 map 处理数组中的每个元素,有了 reduce 可以处理数组最终得到一个值。但是如果想获取数组中的某些元素该怎么办?我们来进一步探索,现在增加一些属性到上面的超级英雄数组中:

const heroes = [
 {name: 'Hulk', strength: 90000, sex: 'm'},
 {name: 'Spider-Man', strength: 25000, sex: 'm'},
 {name: 'Hawk Eye', strength: 136, sex: 'm'},
 {name: 'Thor', strength: 100000, sex: 'm'},
 {name: 'Black Widow', strength: 136, sex: 'f'},
 {name: 'Vision', strength: 5000, sex: 'm'},
 {name: 'Scarlet Witch', strength: 60, sex: 'f'},
 {name: 'Mystique', strength: 120, sex: 'f'},
 {name: 'Namora', strength: 75000, sex: 'f'},
];

ok,现在有两个问题,我们想要:

找到所有的女性英雄;
找到所有能量值大于500的英雄。
使用普通的 for...of 循环,会得到如下代码:

let femaleHeroes = [];
for (let hero of heroes) {
 if (hero.sex === 'f') {
  femaleHeroes.push(hero);
 }
}

let superhumans = [];
for (let hero of heroes) {
 if (hero.strength >= 500) {
  superhumans.push(hero);
 }
}

逻辑严密,看起来还不错?但是里面又出现了重复的情况。实际上,区别在于 if 的判断语句,那么能不能把 if 语句重构到一个函数中呢?

function isFemaleHero(hero) {
 return (hero.sex === 'f');
}

function isSuperhuman(hero) {
 return (hero.strength >= 500);
}

let femaleHeroes = [];
for (let hero of heroes) {
 if (isFemaleHero(hero)) {
  femaleHeroes.push(hero);
 }
}

let superhumans = [];
for (let hero of heroes) {
 if (isSuperhuman(hero)) {
  superhumans.push(hero);
 }
}

这种只返回 true 或者 false 的函数,我们一般把它称作断言(predicate)函数。这里用了断言(predicate)函数来判断是否需要保留当前的英雄。

上面代码的写法会看起来比较长,但是把断言函数抽离出来,可以让重复的循环代码更加明显。现在把种循环抽离到一个函数当中。

function filter(predicate, arr) {
 let working = [];
 for (let item of arr) {
  if (predicate(item)) {
   working = working.concat(item);
  }
 }
}

const femaleHeroes = filter(isFemaleHero, heroes);
const superhumans = filter(isSuperhuman, heroes);

同 map 和 reduce 一样,JavaScript 提供了一个内置数组方法,没必要自己来实现(除非你自己想写)。用内置数组方法,上面的代码就变成了:

const femaleHeroes = heroes.filter(isFemaleHero);
const superhumans = heroes.filter(isSuperhuman);

为什么这段代码比 for...of 循环好呢?回想一下整个过程,我们要解决一个“找到满足某一条件的所有英雄”。使用 filter 使得问题变得简单化了。我们需要做的就是通过写一个简单函数来告诉 filter 哪一个数组元素要保留。不需要考虑数组是什么样的,以及繁琐的中间变量。取而代之的是一个简单的断言函数,仅此而已。

与其他的迭代函数相比,使用 filter 是一个四两拨千斤的过程。我们不需要通读循环代码来理解到底要过滤什么,要过滤的东西就在传递给它的那个函数里面。

finding

filter 已经信手拈来了吧。这时如果只想找一个英雄该怎么办?比如找 “Black Widow”。使用 filter 会这样写:

function isBlackWidow(hero) {
 return (hero.name === 'Black Widow');
}

const blackWidow = heroes.filter(isBlackWidow)[0];

这段代码的问题是效率不够高。filter 会检查数组中的每一个元素,而我们知道这里面只有一个 “Black Widow”,当找到她的时候就可以停住,不用再看后面的元素了。那么,依旧利用断言函数,我们写一个 find 函数来返回第一次匹配上的元素。

function find(predicate, arr) {
 for (let item of arr) {
  if (predicate(item)) {
   return item;
  }
 }
}

const blackWidow = find(isBlackWidow, heroes);

同样地,JavaScript 已经提供了这样的方法:

const blackWidow = heroes.find(isBlackWidow);

find 再次体现了四两拨千斤的特点。通过 find 方法,把问题简化为:你只要关注如何判断你要找的东西就可以了,不必关心迭代到底怎么实现等细节问题。

总结

这些迭代函数的例子很好地诠释“抽象”的作用和优雅。回想一下我们所讲的内置方法,每个例子中我们都做了三件事:

消除了循环结构,使得代码变的简洁易读;
通过适当的方法名称来描述我们使用的模式,也就是:map,reduce,filter 和 find;
把问题从处理整个数组简化到处理每个元素。
注意在每一种情况下,我们都用几个纯函数来分解问题和解决问题。真正令人兴奋的是通过仅仅这么四种模式模式(当然还有其他的模式,也建议大家去学习一下),在 JS 代码中你就可以消除几乎所有的循环了。这是因为 JS 中几乎每个循环都是用来处理数组,或者生成数组的。通过消除循环,降低了复杂性,也使得代码的可维护性更强。

作者:James Sinclair 
编译:胡子大哈

翻译原文:http://huziketang.com/blog/posts/detail?postId=58ad37c3204d50674934c3ab 
英文原文:JAVASCRIPT WITHOUT LOOPS

(0)

相关推荐

  • Hadoop MapReduce多输出详细介绍

    Hadoop MapReduce多输出 FileOutputFormat及其子类产生的文件放在输出目录下.每个reducer一个文件并且文件由分区号命名:part-r-00000,part-r-00001,等等.有时可能要对输出的文件名进行控制或让每个reducer输出多个文件.MapReduce为此提供了MultipleOutputFormat类. MultipleOutputFormat类可以将数据写到多个文件,这些文件的名称源于输出的键和值或者任意字符串.这允许每个reducer(或者只有

  • JavaScript之map reduce_动力节点Java学院整理

    如果你读过Google的那篇大名鼎鼎的论文"MapReduce: Simplified Data Processing on Large Clusters",你就能大概明白map/reduce的概念. map 举例说明,比如我们有一个函数f(x)=x2,要把这个函数作用在一个数组[1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map实现如下: 由于map()方法定义在JavaScript的Array中,我们调用Array的map()方法,传入我们自己的函数,就得到了一

  • 5个数组Array方法: indexOf、filter、forEach、map、reduce使用实例

    ECMAScript5标准发布于2009年12月3日,它带来了一些新的,改善现有的Array数组操作的方法.然而,这些新奇的数组方法并没有真正流行起来的,因为当时市场上缺乏支持ES5的浏览器. Array "Extras" 没有人怀疑这些方法的实用性,但写polyfill(PS:兼容旧版浏览器的插件)对他们来说是不值得的.它把"必须实现"变成了"最好实现".有人居然将这些数组方法称之为Array "Extras".哎! 但是,

  • JavaScript mapreduce工作原理简析

    谷歌在2003到2006年间连续发表了三篇非常有影响力的文章,分别是2003年在SOSP上发布的GFS,2004年在OSDI上发布的MapReduce,以及2006年在OSDI上发布的BigTable.GFS是文件系统相关的,其对后来的分布式文件系统设计具有指导意义:MapReduce是一种并行计算的编程模型,用于作业调度:BigTable是一个用于管理结构化数据的分布式存储系统,构建在GFS.Chubby.SSTable等Google技术之上.相当多的Google应用使用了这三种技术,比如Go

  • 无循环 JavaScript(map、reduce、filter和find)

    之前有讨论过,缩进(非常粗鲁地)增加了代码复杂性.我们的目标是写出复杂度低的 JavaScript 代码.通过选择一种合适的抽象来解决这个问题,可是你怎么能知道选择哪一种抽象呢?很遗憾的是到目前为止,没有找到一个具体的例子能回答这个问题.这篇文章中我们讨论不用任何循环如何处理 JavaScript 数组,最终得出的效果是可以降低代码复杂性. 循环是一种很重要的控制结构,它很难被重用,也很难插入到其他操作之中.另外,它意味着随着每次迭代,代码也在不断的变化之中.--Luis Atencio 我们先

  • Python3的高阶函数map,reduce,filter的示例详解

    函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数. 注意其中:map和filter返回一个惰性序列,可迭代对象,需要转化为list >>> a = 3.1415 >>> round(a,2) 3.14 >>> a_round = round >>> a_round(a,2) 3.14 >>> def func_devide(x, y, f): return f(x) - f(y

  • python中三种高阶函数(map,reduce,filter)详解

    map(function,seq[,seq2]) 接收至少两个参数,基本作用为将传入的函数依次作用到序列的每个元素,并且把结果作为新的序列 返回一个可迭代的map对象 function:函数对象 py2中可为None,作用等同于zip() 如: py3中不可为None,None是不可调用.不可迭代对象 seq:可迭代对象,可以传一个或多个 # 传一个: def func(i):return i*2 print([i for i in map(func,[1,'2'])]) # [2,'22']

  • Python3中map(),reduce(),filter()的详细用法

    目录 前言 1.map() 2.filter() 3.reduce() 前言 Python3中的map().reduce().filter() 这3个一般是用于对序列进行操作的内置函数,它们经常需要与 匿名函数 lambda 联合起来使用,我们今天就来学习下. 1.map() map() 可以用于在函数中对指定序列做映射,返回值是一个迭代器,其使用语法如下: map(function, *iterables) 上面的第一个参数 function 指一个函数,第二个参数 iterable 指一个或

  • python 内置函数-range()+zip()+sorted()+map()+reduce()+filter()

    目录 range函数 zip() 函数 其它内置函数 数据类型转换相关内置函数 变量相关函数 数学相关函数 进制相关函数 高阶函数 sorted(iterable,[reverse,key]) map(func, *iterables) reduce(func,iterable) filter(func,iterable) range函数 能够生成一个指定的数字序列 使用案例: ''' range(start,stop,step) 参数: start : 开始的值 ,默认值为0 stop : 结

  • Python中map,reduce,filter和sorted函数的使用方法

    map map(funcname, list) python的map 函数使得函数能直接以list的每个元素作为参数传递到funcname中, 并返回响应的新的list 如下: def sq(x): return x*x #求x的平方 map(sq, [1,3, 5,7,9]) #[1, 9, 25, 49, 81] 在需要对list中的每个元素做转换的时候, 会很方便 比如,把list中的每个int 转换成str map(str, [23,43,4545,324]) #['23', '43',

  • Python学习之魔法函数(filter,map,reduce)详解

    目录 filter() 函数 map() 函数 reduce() 函数 filter() 函数 小实战 今天的这一章节我们来学习一下,Python 中的三个高级函数,也被称之为 魔法函数.之所以把他们交的这么高级,主要是因为它们返回的数据类型多数是 迭代器. 我们在上一章节有介绍过,迭代器 可以提升我们的代码的执行效率.降低内存消耗.所以接下来我们就认识一下这些 魔法函数. filter() 函数 filter() 函数 是python的一个内置函数. filter() 函数的功能:可以将一个可

  • JavaScript 数组方法filter与reduce

    目录 前言 filter reduce 数组求和 筛选首字母是否是含有b字母 结语 前言 在ES6新增的数组方法中,包含了多个遍历方法,其中包含了用于筛选的filter和reduce filter 主要用于筛选数组的filter方法,在使用中,不会改变原数组,同时会将符合筛选条件的元素,放入新的数组进行返回. /*** * @item 数组元素 * @index 遍历数组下标 * @thisArr 当前数组 */ let arr1 = [1, 2, 3, 4, 5]; let newArr1 =

  • python中几个常用函数的正确用法-lambda/filter/map/reduce

    目录 1 lambda 2 filter 3 map 4 reduce 5 联合使用 lambda/filter/map/reduce这几个函数面试中很肯定会用到,本篇主要介绍这几个函数的用法. 1 lambda 匿名函数,用法如下: # lambada 参数,参数,参数 : 返回的表达式 示例1: f = lambda x, y: x * y print(f(2, 3)) # 6 示例2: r = (lambda x, y: x+y)(1, 2) print(r) # 3 2 filter f

  • python中filter,map,reduce的作用

    目录 一.map函数 1. lambda函数 2. 自定义函数 二.filter函数 1. lambda函数 2. 自定义函数 三.reduce函数 1. lambda函数 2. 自定义函数 一.map函数 作用:map主要作用是计算一个序列或者多个序列进行函数映射之后的值 语法:map(function,iterable1,iterable2) 说明:function中参数值可以是一个,也可以是多个:iterable代表function运算中的参数值,有几个参数值就传入几个iterable 注

随机推荐