无循环 JavaScript

做者:James Sinclair

编译:胡子大哈 javascript

翻译原文:huziketang.com/blog/posts/…

英文原文:JAVASCRIPT WITHOUT LOOPSjava

转载请注明出处,保留原文连接以及做者信息react


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

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

循环

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

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

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

const input = [
    'John',
    'Paul',
    'George',
    'Ringo',
];复制代码

如今有了一个数组,咱们想要用 oodlify 函数处理每个元素。若是用 while 循环,就相似于这样:ide

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() 函数,而且将结果放到一个新的数组中。oop

对一个数组中每一个元素都进行操做的这种模式也是很是广泛的。所以在 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);复制代码

能够注意到,缩进消失,循环消失。固然循环可能转移到了其余地方,可是咱们已经不须要去关心它们了。如今的代码简洁有力,完美。

为何这个代码这么简单呢?这多是个很傻的问题,不过也请思考一下。是由于短吗?不是,简洁并不表明不复杂。它的简单是由于咱们把问题分离了。有两个处理字符串的函数: oodlifyizzlify,这些函数并不须要知道关于数组或者循环的任何事情。同时,有另一个函数: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,如今有两个问题,咱们想要:

  1. 找到全部的女性英雄;
  2. 找到全部能量值大于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);复制代码

mapreduce 同样,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 方法,把问题简化为:你只要关注如何判断你要找的东西就能够了,没必要关心迭代到底怎么实现等细节问题。

总结

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

  1. 消除了循环结构,使得代码变的简洁易读;
  2. 经过适当的方法名称来描述咱们使用的模式,也就是:mapreducefilterfind
  3. 把问题从处理整个数组简化处处理每一个元素。

注意在每一种状况下,咱们都用几个纯函数来分解问题和解决问题。真正使人兴奋的是经过仅仅这么四种模式模式(固然还有其余的模式,也建议你们去学习一下),在 JS 代码中你就能够消除几乎全部的循环了。这是由于 JS 中几乎每一个循环都是用来处理数组,或者生成数组的。经过消除循环,下降了复杂性,也使得代码的可维护性更强。


我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点

相关文章
相关标签/搜索