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

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

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

JavaScript 輕量級函數式編程

第 11 章:融會貫通

現在你已經掌握了所有需要掌握的關於 JavaScript 輕量級函數式編程的內容。下面不會再引入新的概念。

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

建議進行大量深入的練習來熟悉這些技巧,因爲理解本章內容對於將來你在實際編程場景中應用函數式編程原理至關重要。

準備

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

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

首先來編寫 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實戰項目教程》
包含:文章、視頻、源代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章