無循環 JavaScript

做者:James Sinclair <br/>
編譯:鬍子大哈 javascript

翻譯原文:http://huziketang.com/blog/posts/detail?postId=58ad37c3204d50674934c3ab <br/>
英文原文: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 感興趣的童鞋,歡迎指點

相關文章
相關標籤/搜索