翻譯連載 | 第 11 章:融會貫通 -《JavaScript輕量級函數式編程》 |《你不知道的JS》姊妹篇

關於譯者:這是一個流淌着滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裏最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。通過捶打磨練,成就了本書的中文版。本書包含了函數式編程之精髓,但願能夠幫助你們在學習函數式編程的道路上走的更順暢。比心。javascript

譯者團隊(排名不分前後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌zhouyaohtml

JavaScript 輕量級函數式編程

第 11 章:融會貫通

如今你已經掌握了全部須要掌握的關於 JavaScript 輕量級函數式編程的內容。下面不會再引入新的概念。前端

本章主要目標是概念的融會貫通。經過研究代碼片斷,咱們將本書中大部分主要概念聯繫起來並學以至用。java

建議進行大量深刻的練習來熟悉這些技巧,由於理解本章內容對於未來你在實際編程場景中應用函數式編程原理相當重要。node

準備

咱們來寫一個簡單的股票行情工具吧。git

注意: 能夠在本書的 GitHub 倉庫(https://github.com/getify/Functional-Light-JS)下的 ch11-code/ 目錄裏找到參考代碼。同時,在書中討論到的函數式編程輔助函數的基礎上,咱們篩選了所需的一部分放到了 ch11-code/fp-helpers.js 文件中。本章中,咱們只會討論到其中相關的部分。github

首先來編寫 HTML 部分,這樣即可以對信息進行展現了。咱們在 ch11-code/index.html 文件中先寫一個空的 <ul ..> 元素,在運行時,DOM 會被填充成:編程

<ul id="stock-ticker">
    <li class="stock" data-stock-id="AAPL">
        <span class="stock-name">AAPL</span>
        <span class="stock-price">$121.95</span>
        <span class="stock-change">+0.01</span>
    </li>
    <li class="stock" data-stock-id="MSFT">
        <span class="stock-name">MSFT</span>
        <span class="stock-price">$65.78</span>
        <span class="stock-change">+1.51</span>
    </li>
    <li class="stock" data-stock-id="GOOG">
        <span class="stock-name">GOOG</span>
        <span class="stock-price">$821.31</span>
        <span class="stock-change">-8.84</span>
    </li>
</ul>

我必需要事先提醒你的一點是,和 DOM 進行交互屬於輸入/輸出操做,這也意味着會產生必定的反作用。咱們不能消除這些反作用,因此咱們儘可能減小和 DOM 相關的操做。這些技巧在第 5 章中已經提到了。小程序

歸納一下咱們的小工具的功能:代碼將在每次收到添加新股票事件時添加 <li ..> 元素,並在股票價格更新事件發生時更新價格。微信小程序

在第 11 章的示例代碼 ch11-code/mock-server.js 中,咱們設置了一些定時器,把隨機生成的假股票數據推送到一個簡單的事件發送器中,來模擬從服務器收到的股票數據。咱們暴露了一個 connectToServer() 接口來實現模擬,可是實際上,它只是返回了一個假的事件發送器。

注意: 這個文件是用來模擬數據的,因此我沒有花費太多的精力讓它徹底符合函數式編程,不建議你們花太多時間研究這個文件中的代碼。若是你寫了一個真正的服務器 —— 對於那些雄心勃勃的讀者來講,這是一個有趣的加分練習 —— 這時你才應該考慮採用函數式編程思想來實現這些代碼。

咱們在 ch11-code/stock-ticker-events.js 中,建立了一些 observable(經過 RxJS)鏈接到事件發送器對象上。經過調用 connectToServer() 來獲取這個事件的發射器,而後監聽名稱爲 "stock" 的事件,經過這個事件來添加一個新的股票代碼,同時監聽名稱爲 "stock-update" 的事件,經過這個事件來更新股票價格和漲跌幅。最後,咱們定義一些轉換函數,來對這些 observable 傳入的數據進行格式化。

ch11-code/stock-ticker.js 中,咱們將咱們的界面操做(DOM 部分的反作用)定義在 stockTickerUI 對象的方法中。咱們還定義了各類輔助函數,包括 getElemAttr(..)stripPrefix(..) 等等。最後,咱們經過 subscribe(..) 監聽兩個 observable,來得到格式化好的數據,渲染到 DOM 上。

股票信息

一塊兒看看 ch11-code/stock-ticker-events.js 中的代碼,咱們先從一些基本的輔助函數開始:

function addStockName(stock) {
    return setProp( "name", stock, stock.id );
}
function formatSign(val) {
    if (Number(val) > 0) {
        return `+${val}`;
    }
    return val;
}
function formatCurrency(val) {
    return `$${val}`;
}
function transformObservable(mapperFn,obsv){
    return obsv.map( mapperFn );
}

這些純函數應該很容易理解。參見第 4 章 setProp(..) 在設置新屬性以前複製了對象。這實踐到了咱們在第 6 章中學習到的原則:經過把變量看成不可變的變量來避免反作用,即便其自己是可變的。

addStockName(..) 用來在股票信息對象中添加一個 name 屬性,它的值和這個對象 id 一致。name 會做爲股票的名稱展現在工具中。

有一個關於 transformObservable(..) 的頗爲微妙的注意事項:表面上看起來在 map(..) 函數中返回一個新的 observable 是純函數操做,可是事實上,obsv 的內部狀態被改變了,這樣纔可以和 map(..) 返回的新的 observable 鏈接起來。這個反作用並非個大問題,並且不會影響咱們的代碼可讀性,可是隨時發現潛在的反作用是很是重要的,這樣就不會在出錯時倍感驚訝!

當從「服務器」獲取股票信息時,數據是這樣的:

{ id: "AAPL", price: 121.7, change: 0.01 }

在把 price 的值顯示到 DOM 上以前,須要用 formatCurrency(..) 函數格式化一下(好比變成 "$121.70"),同時須要用 formatChange(..) 函數格式化 change 的值(好比變成 "+0.01")。可是咱們不但願修改消息對象中的 pricechange,因此咱們須要一個輔助函數來格式化這些數字,而且要求這個輔助函數返回一個新的消息對象,其中包含格式化好的 pricechange

function formatStockNumbers(stock) {
    var updateTuples = [
        [ "price", formatPrice( stock.price ) ],
        [ "change", formatChange( stock.change ) ]
    ];

    return reduce( function formatter(stock,[propName,val]){
        return setProp( propName, stock, val );
    } )
    ( stock )
    ( updateTuples );
}

咱們建立了 updateTuples 元組來保存 pricechange 的信息,包括屬性名稱和格式化好的值。把 stock 對象做爲 initialValue,對元組進行 reduce(..)(參考第 8 章)。把元組中的信息解構成 propNameval,而後返回了 setProp(..) 調用的結果,這個結果是一個被複制了的新的對象,其中的屬性被修改過了。

下面咱們再定義幾個輔助函數:

var formatDecimal = unboundMethod( "toFixed" )( 2 );
var formatPrice = pipe( formatDecimal, formatCurrency );
var formatChange = pipe( formatDecimal, formatSign );
var processNewStock = pipe( addStockName, formatStockNumbers );

formatDecimal(..) 函數接收一個數字做爲參數(如 2.1)而且調用數字的 toFixed( 2 ) 方法。咱們使用了第 8 章介紹的 unboundMethod(..) 來建立一個獨立的延遲綁定函數。

formatPrice(..)formatChange(..)processNewStock(..) 都用到了 pipe(..) 來從左到右地組合運算(見第 4 章)。

爲了能在事件發送器的基礎上建立 observable(見第 10 章),咱們將封裝一個獨立的柯里化輔助函數(見第 3 章)來包裝 RxJS 的 Rx.Observable.fromEvent(..)

var makeObservableFromEvent = curry( Rx.Observable.fromEvent, 2 )( server );

這個函數特定地監聽了 server(事件發送器),在接受了事件名稱字符串參數後,就能生成 observable 了。咱們準備好了建立 observer 的全部代碼片斷後,用映射函數轉換 observer 來格式化獲取到的數據:

var observableMapperFns = [ processNewStock, formatStockNumbers ];

var [ newStocks, stockUpdates ] = pipe(
    map( makeObservableFromEvent ),
    curry( zip )( observableMapperFns ),
    map( spreadArgs( transformObservable ) )
)
( [ "stock", "stock-update" ] );

咱們建立了包含了事件名稱(["stock","stock-update"])的數組,而後 map(..)(見第 8 章)這個數組,生成了一個包含了兩個 observable 的數組,而後把這個數組和 observable 映射函數 zip(..)(見第 8 章)起來,產生一個 [ observable, mapperFn ] 這樣的元組數組。最後經過 spreadArgs(..)(見第 3 章)把每一個元組數組展開爲單獨的參數,map(..) 到了 transformObservable(..) 函數上。

獲得的結果是一個包含了轉換好的 observable 的數組,經過數組結構賦值的方式分別賦值到了 newStocksstockUpdates 兩個變量上。

到此爲止,咱們用輕量級函數式編程的方式來讓股票行情信息事件成爲了 observable!在 ch11-code/stock-ticker.js 中咱們會訂閱這兩個 observable。

回頭想一想咱們用到的函數式編程原則。這樣作有沒有意義呢?你可否明白咱們是如何運用前幾章中介紹的各類概念的呢?你能不能想到別的方式來實現這些功能?

更重要的是,若是你用命令式編程的方法是如何實現上面的功能的呢?你認爲兩種方式相比孰優孰劣?試試看用你熟悉的命令式編程的方式去寫這個功能。若是你和我同樣,那麼命令式編程仍然會讓你感到更加天然。

在進行下面的學習以前,你須要明白的是,除了使你感到很是天然的命令式編程之外,你已經可以瞭解函數式編程的合理性了。想一想看每一個函數的輸入和輸出,你看到它們是怎樣組合在一塊兒的了嗎?

在你豁然開朗之前必定要持續不斷地練習。

股票行情界面

若是你熟悉了上一章節中的函數式編程模式,你就能夠開始學習 ch11-code/stock-ticker.js 文件中的內容了。這裏會涉及至關多的重要內容,因此咱們將好好地理解整個文件中的每一個方法。

咱們先從定義一些操做 DOM 的輔助函數開始:

function isTextNode(node) {
    return node && node.nodeType == 3;
}
function getElemAttr(elem,prop) {
    return elem.getAttribute( prop );
}
function setElemAttr(elem,prop,val) {
    // 反作用!!
    return elem.setAttribute( prop, val );
}
function matchingStockId(id) {
    return function isStock(node){
        return getStockId( node ) == id;
    };
}
function isStockInfoChildElem(elem) {
    return /\bstock-/i.test( getClassName( elem ) );
}
function appendDOMChild(parentNode,childNode) {
    // 反作用!!
    parentNode.appendChild( childNode );
    return parentNode;
}
function setDOMContent(elem,html) {
    // 反作用!!
    elem.innerHTML = html;
    return elem;
}

var createElement = document.createElement.bind( document );

var getElemAttrByName = curry( reverseArgs( getElemAttr ), 2 );
var getStockId = getElemAttrByName( "data-stock-id" );
var getClassName = getElemAttrByName( "class" );

這些函數應該算是不言自明的。爲了得到 getElemAttrByName(..),我用了 curry(reverseArgs( .. ))(見第 3 章)而不是 partialRight(..),只是爲了在這種特殊狀況下,稍微提升一點性能。

注意,我標出了操做 DOM 元素時的反作用。由於不能簡單地用克隆的 DOM 對象去替換已有的,因此咱們在不替換已有對象的基礎上,勉強接受了一些反作用的產生。至少若是在 DOM 渲染中產生一個錯誤,咱們能夠輕鬆地搜索這些代碼註釋來縮小可能的錯誤代碼。

matchingStockId(..) 用到了閉包(見第 2 章),它建立了一個內部函數(isStock(..)),使在其餘做用域下運行時依然可以保存 id 變量。

其餘的輔助函數:

function stripPrefix(prefixRegex) {
    return function mapperFn(val) {
        return val.replace( prefixRegex, "" );
    };
}
function listify(listOrItem) {
    if (!Array.isArray( listOrItem )) {
        return [ listOrItem ];
    }
    return listOrItem;
}

定義一個用以獲取某個 DOM 元素的子節點的輔助函數:

var getDOMChildren = pipe(
    listify,
    flatMap(
        pipe(
            curry( prop )( "childNodes" ),
            Array.from
        )
    )
);

首先,用 listify(..) 來保證咱們獲得的是一個數組(即便裏面只有一個元素)。回憶一下在第 8 章中提到的 flatMap(..),這個函數把一個包含數組的數組扁平化,變成一個淺數組。

映射函數先把 DOM 元素映射成它的子元素數組,而後咱們用 Array.from(..) 把這個數組變成一個真實的數組(而不是一個 NodeList)。這兩個函數組合成一個映射函數(經過 pipe(..)),這就是融合(見第 8 章)。

如今,咱們用 getDOMChildren(..) 實用函數來定義股票行情工具中查找特定 DOM 元素的工具函數:

function getStockElem(tickerElem,stockId) {
    return pipe(
        getDOMChildren,
        filterOut( isTextNode ),
        filterIn( matchingStockId( stockId ) )
    )
    ( tickerElem );
}
function getStockInfoChildElems(stockElem) {
    return pipe(
        getDOMChildren,
        filterOut( isTextNode ),
        filterIn( isStockInfoChildElem )
    )
    ( stockElem );
}

getStockElem(..) 接受 tickerElem DOM 節點做爲參數,獲取其子元素,而後過濾,保證咱們獲得的是符合股票代碼的 DOM 元素。getStockInfoChildElems(..) 幾乎是同樣的,不一樣的是它從一個股票元素節點開始查找,還使用了不一樣的過濾函數。

兩個實用函數都會過濾掉文字節點(由於它們沒有其餘的 DOM 節點那樣的方法),保證返回一個 DOM 元素數組,哪怕數組中只有一個元素。

主函數

咱們用 stockTickerUI 對象來保存三個修改界面的主要方法,以下:

var stockTickerUI = {

    updateStockElems(stockInfoChildElemList,data) {
        // ..
    },

    updateStock(tickerElem,data) {
        // ..
    },

    addStock(tickerElem,data) {
        // ..
    }
};

咱們先看看 updateStock(..),這是三個函數裏面最簡單的:

var stockTickerUI = {

    // ..

    updateStock(tickerElem,data) {
        var getStockElemFromId = curry( getStockElem )( tickerElem );
        var stockInfoChildElemList = pipe(
            getStockElemFromId,
            getStockInfoChildElems
        )
        ( data.id );

        return stockTickerUI.updateStockElems(
            stockInfoChildElemList,
            data
        );
    },

    // ..

};

柯里化以前的輔助函數 getStockElem(..),傳給它 tickerElem,獲得了 getStockElemFromId(..) 函數,這個函數接受 data.id 做爲參數。把 <li> 元素(實際上是數組形式的)傳入 getStockInfoChildElems(..),咱們獲得了三個 <span> 子元素,用來展現股票信息,咱們把它們保存在 stockInfoChildElemList 變量中。而後把數組和股票信息 data 對象一塊兒傳給 stockTickerUI.updateStockElems(..),來更新 <span> 中的數據。

如今咱們來看看 stockTickerUI.updateStockElems(..)

var stockTickerUI = {

    updateStockElems(stockInfoChildElemList,data) {
        var getDataVal = curry( reverseArgs( prop ), 2 )( data );
        var extractInfoChildElemVal = pipe(
            getClassName,
            stripPrefix( /\bstock-/i ),
            getDataVal
        );
        var orderedDataVals =
            map( extractInfoChildElemVal )( stockInfoChildElemList );
        var elemsValsTuples =
            filterOut( function updateValueMissing([infoChildElem,val]){
                return val === undefined;
            } )
            ( zip( stockInfoChildElemList, orderedDataVals ) );

        // 反作用!!
        compose( each, spreadArgs )
        ( setDOMContent )
        ( elemsValsTuples );
    },

    // ..

};

這部分有點難理解。咱們一行行來看。

首先把 prop 函數的參數反轉,柯里化後,把 data 消息對象綁定上去,獲得了 getDataVal(..) 函數,這個函數接收一個屬性名稱做爲參數,返回 data 中的對應的屬性名稱的值。

接下來,咱們看看 extractInfoChildElem

var extractInfoChildElemVal = pipe(
    getClassName,
    stripPrefix( /\bstock-/i ),
    getDataVal
);

這個函數接受一個 DOM 元素做爲參數,拿到 class 屬性的值,而後把 "stock-" 前綴去掉,而後用這個屬性值("name""price""change"),經過 getDataVal(..) 函數,在 data 中找到對應的數據。你可能會問:「還有這種操做?」。

其實,這麼作的目的是按照 stockInfoChildElemList 中的 <span> 元素的順序從 data 中拿到數據。咱們對 stockInfoChildElemList 數組調用 extractInfoChildElem 映射函數,來拿到這些數據。

接下來,咱們把 <span> 數組和數據數組壓縮起來,獲得一個元組:

zip( stockInfoChildElemList, orderedDataVals )

這裏有一點不太容易理解,咱們定義的 observable 轉換函數中,新的股票行情數據 data 會包含一個 name 屬性,來對應 <span class="stock-name"> 元素,可是在股票行情更新事件的數據中可能會找不到對應的 name 屬性。

通常來講,若是股票更新消息事件的數據對象不包含某個股票數據的話,咱們就不該該更新這隻股票對應的 DOM 元素。因此咱們要用 filterOut(..) 剔除掉沒有值的元組(這裏的值在元組的第二個元素)。

var elemsValsTuples =
    filterOut( function updateValueMissing([infoChildElem,val]){
        return val === undefined;
    } )
    ( zip( stockInfoChildElemList, orderedDataVals ) );

篩選後的結果是一個元組數組(如:[ <span>, ".." ]),這個數組能夠用來更新 DOM 了,咱們把這個結果保存到 elemsValsTuples 變量中。

注意: 既然 updateValueMissing(..) 是聲明在函數內的,因此咱們能夠更方便地控制這個函數。與其使用 spreadArgs(..) 來把函數接收的一個數組形式的參數展開成兩個參數,咱們能夠直接用函數的參數解構聲明(function updateValueMissing([infoChildElem,val]){ ..),參見第 2 章。

最後,咱們要更新 DOM 中的 <span> 元素:

// 反作用!!
compose( each, spreadArgs )( setDOMContent )
( elemsValsTuples );

咱們用 each(..) 遍歷了 elemsValsTuples 數組(參考第 8 章中關於 forEach(..) 的討論)。

與其餘地方使用 pipe(..) 來組合函數不一樣,這裏使用 compose(..)(見第 4 章),先把 setDomContent(..) 傳到 spreadArgs(..) 中,再把執行的結果做爲迭代函數傳到 each(..) 中。執行時,每一個元組被展開爲參數傳給了 setDOMContent(..) 函數,而後對應地更新 DOM 元素。

最後說明下 addStock(..)。咱們先把整個函數寫出來,而後再一句句地解釋:

var stockTickerUI = {

    // ..

    addStock(tickerElem,data) {
        var [stockElem, ...infoChildElems] = map(
            createElement
        )
        ( [ "li", "span", "span", "span" ] );
        var attrValTuples = [
            [ ["class","stock"], ["data-stock-id",data.id] ],
            [ ["class","stock-name"] ],
            [ ["class","stock-price"] ],
            [ ["class","stock-change"] ]
        ];
        var elemsAttrsTuples =
            zip( [stockElem, ...infoChildElems], attrValTuples );

        // 反作用!!
        each( function setElemAttrs([elem,attrValTupleList]){
            each(
                spreadArgs( partial( setElemAttr, elem ) )
            )
            ( attrValTupleList );
        } )
        ( elemsAttrsTuples );

        // 反作用!!
        stockTickerUI.updateStockElems( infoChildElems, data );
        reduce( appendDOMChild )( stockElem )( infoChildElems );
        tickerElem.appendChild( stockElem );
    }

};

這個操做界面的函數會根據新的股票信息生成一個空的 DOM 結構,而後調用 stockTickerUI.updateStockElems(..) 方法來更新其中的內容。

首先:

var [stockElem, ...infoChildElems] = map(
    createElement
)
( [ "li", "span", "span", "span" ] );

咱們先建立 <li> 父元素和三個 <span> 子元素,把它們分別賦值給了 stockEleminfoChildElems 數組。

爲了設置 DOM 元素的對應屬性,咱們聲明瞭一個元組數組組成的數組。按照順序,每一個元組數組對應上面四個 DOM 元素中的一個。每一個元組數組中的元組由對應元素的屬性和值組成:

var attrValTuples = [
    [ ["class","stock"], ["data-stock-id",data.id] ],
    [ ["class","stock-name"] ],
    [ ["class","stock-price"] ],
    [ ["class","stock-change"] ]
];

咱們把四個 DOM 元素和 attrValTuples 數組 zip(..) 起來:

var elemsAttrsTuples =
    zip( [stockElem, ...infoChildElems], attrValTuples );

最後的結果會是:

[
    [ <li>, [ ["class","stock"], ["data-stock-id",data.id] ] ],
    [ <span>, [ ["class","stock-name"] ] ],
    ..
]

若是咱們用命令式的方式來把屬性和值設置到每一個 DOM 元素上,咱們會用嵌套的 for 循環。用函數式編程的方式的話也會是這樣,不過這時嵌套的是 each(..) 循環:

// 反作用!!
each( function setElemAttrs([elem,attrValTupleList]){
    each(
        spreadArgs( partial( setElemAttr, elem ) )
    )
    ( attrValTupleList );
} )
( elemsAttrsTuples );

外層的 each(..) 循環了元組數組,其中每一個數組的元素是一個 elem 和它對應的 attrValTupleList,這個元組數組被傳入了 setElemAttrs(..),在函數的參數中被解構成兩個值。

在外層循環內,元組數組的子數組(包含了屬性和值的數組)被傳遞到了內層的 each(..) 循環中。內層的迭代函數首先以 elem 做爲第一個參數對 setElemAttr(..) 進行了部分實現,而後把剩下的函數參數展開,把每一個屬性值元組做爲參數傳遞進這個函數中。

到此爲止,咱們有了 <span> 元素數組,每一個元素上都有了該有的屬性,可是尚未 innerHTML 的內容。這裏,咱們要用 stockTickerUI.updateStockElems(..) 函數,把 data 設置到 <span> 上去,和股票信息更新事件的處理同樣。

而後,咱們要把這些 <span> 元素添加到對應的父級 <li> 元素中去,咱們用 reduce(..) 來作這件事(見第 8 章)。

reduce( appendDOMChild )( stockElem )( infoChildElems );

最後,用操做 DOM 元素的反作用方法把新的股票元素添加到小工具的 DOM 節點中去:

tickerElem.appendChild( stockElem );

呼!你跟上了嗎?我建議你在繼續下去以前,回到開頭,從新讀幾遍這部份內容,再練習幾遍。

訂閱 Observable

最後一個重要任務是訂閱 ch11-code/stock-ticker-events.js 中定義的 observable,把事件傳遞給正確的主函數(addStock(..)updateStock(..))。

注意,這兩個主函數接受 tickerElem 做爲第一個參數。咱們聲明一個數組(stockTickerUIMethodsWithDOMContext)保存了兩個中間函數(也叫做閉包,見第 2 章),這兩個中間函數是經過部分參數綁定的函數把小工具的 DOM 元素綁定到了兩個主函數上來生成的。

var ticker = document.getElementById( "stock-ticker" );

var stockTickerUIMethodsWithDOMContext = map(
    curry( reverseArgs( partial ), 2 )( ticker )
)
( [ stockTickerUI.addStock, stockTickerUI.updateStock ] );

reverseArgs( partial ) 是以前提到的 partialRight(..) 的替代品,優化了性能。可是這裏 partial(..) 是映射函數的目標函數。因此咱們須要事先 curry(..) 化,這樣咱們就能夠先把第二個參數 ticker 傳給 partial(..),後面把主函數傳進去的時候就能夠用到以前傳入的 ticker 了。數組中的這兩個中間函數就能夠被用來訂閱 observable 了。

咱們用閉包在這兩個中間函數中保存了 ticker 數據,在第 7 章中,咱們知道了還能夠把 ticker 保存在對象的屬性上,經過使用兩個函數上的指向 stockTickerUIthis 來訪問 ticker。由於 this 是個隱式的輸入(見第 2 章),因此通常來講不推薦用對象的方式,因此我使用了閉包的方式。

爲了訂閱 observable,咱們先寫一個輔助函數,提供一個未綁定的方法:

var subscribeToObservable =
    pipe( uncurry, spreadArgs )( unboundMethod( "subscribe" ) );

unboundMethod("subscribe") 已經柯里化了,因此咱們用 uncurry(..)(見第 3 章)先反柯里化,而後再用 spreadArgs(..)(依然見第 3 章)來修改接受的參數的格式,因此這個函數接受一個元組做爲參數,展開後傳遞下去。

如今,咱們只要把 observable 數組和封裝好上下文的主函數 zip(..) 起來。生成一個元組數組,每一個元組能夠用以前定義的 subscribeToObservable(..) 輔助函數來訂閱 observable:

var stockTickerObservables = [ newStocks, stockUpdates ];

// 反作用!!
each( subscribeToObservable )
( zip( stockTickerUIMethodsWithDOMContext, stockTickerObservables ) );

因爲咱們修改了這些 observable 的狀態以訂閱它們,並且因爲咱們使用了 each(..) —— 老是和反作用相關! —— 咱們用代碼註釋來講明這個問題。

就是這樣!花些時間研究比較這段代碼和它命令式的替代版本,正如咱們以前在股票行情信息中討論到的同樣。真的,能夠多花點時間。我知道這是一本很長的書,可是完整地讀下來會讓你可以消化和理解這樣的代碼。

你如今打算在 JavaScript 中如何合理地使用函數式編程?繼續練習,就像咱們在這裏作的同樣!

總結

咱們在本章中討論的示例代碼應該被做爲一個總體來閱讀,而不只僅是做爲章節中所展現的支離破碎的代碼片斷。若是你尚未完整地閱讀過,如今請停下來,去完整地閱讀一遍代碼目錄下的文件吧。確保你在完整的上下文中瞭解它們。

示例代碼並非實際編寫代碼的範例,只是提供了一種描述性的,教授如何用輕量級函數式的技巧來解決此類問題的方法。這些代碼儘量多地把本書中不一樣概念聯繫起來。這裏提供了比代碼片斷更真實的例子來學習函數式編程。

我相信,隨着我不斷地學習函數式編程,我會繼續改進這個示例代碼。你如今看到的只是我在學習曲線上的一個快照。我但願對你來講也是如此。

在咱們結束本書的主要內容時,咱們一塊兒回顧一下我在第 1 章中提到的可讀性曲線:

在學習函數式編程的過程當中,理解這張圖的真諦,而且爲本身設定合理的預期,是很是重要的。你已經到這裏了,這已是一個很大的成果了。

可是,當你在絕望和沮喪的低谷時,別停下來。前面等待你的是一種更好的思惟方式,能夠寫出可讀性更好,更容易理解,更容易驗證,最終更加可靠的代碼。

我不須要再爲開發者們不斷前行想出更多崇高的理由。感謝你參與到我學習 JavaScript 中的函數式編程的原理的過程當中來。我但願你的學習過程和個人同樣,充實而充滿但願!

** 【上一章】翻譯連載 | 第 10 章:異步的函數式(下)-《JavaScript輕量級函數式編程》 |《你不知道的JS》姊妹篇 **

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

iKcamp官網:https://www.ikcamp.com 訪問官網更快閱讀所有免費分享課程: 《iKcamp出品|全網最新|微信小程序|基於最新版1.0開發者工具之初中級培訓教程分享》 《iKcamp出品|基於Koa2搭建Node.js實戰項目教程》 包含:文章、視頻、源代碼

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息