无轮回 JavaScript

作者:James Sinclair <br/>
编译:胡子大哈

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

转载请说明出处,保留原文链接以及作者信息

之前有议论过,缩进(异常粗暴地)增添了代码庞杂性。我们的目的是写出庞杂度低的 JavaScript 代码。经由历程挑选一种适宜的笼统来处置惩罚这个题目,然则你怎样能晓得挑选哪种笼统呢?很遗憾的是到目前为止,没有找到一个详细的例子能回复这个题目。这篇文章中我们议论没必要任何轮回怎样处置惩罚 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);

能够注重到,缩进消逝,轮回消逝。固然轮回能够转移到了其他地方,然则我们已不须要去体贴它们了。如今的代码简约有力,圆满。

为何这个代码这么简朴呢?这多是个很傻的题目,不过也请思索一下。是由于短吗?不是,简约并不代表不庞杂。它的简朴是由于我们把题目星散了。有两个处置惩罚字符串的函数: 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 感兴趣的童鞋,迎接指导

    原文作者:胡子大哈
    原文地址: https://segmentfault.com/a/1190000008440954
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞