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

iKcamp發表於2017-11-16

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

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

JavaScript 輕量級函數語言程式設計

第 11 章:融會貫通

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

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

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

準備

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

注意: 可以在本書的 GitHub 倉庫(github.com/getify/Func…)下的 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 章中提到的可讀性曲線:

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

在學習函數語言程式設計的過程中,理解這張圖的真諦,並且為自己設定合理的預期,是非常重要的。你已經到這裡了,這已經是一個很大的成果了。

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

我不需要再為開發者們不斷前行想出更多崇高的理由。感謝你參與到我學習 JavaScript 中的函數語言程式設計的原理的過程中來。我希望你的學習過程和我的一樣,充實而充滿希望!

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

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

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

iKcamp官網:www.ikcamp.com 訪問官網更快閱讀全部免費分享課程: 《iKcamp出品|全網最新|微信小程式|基於最新版1.0開發者工具之初中級培訓教程分享》 《iKcamp出品|基於Koa2搭建Node.js實戰專案教程》 包含:文章、視訊、原始碼


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

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章