翻译连载 | 第 11 章:融会贯通 -《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。通过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,但愿能够帮助你们在学习函数式编程的道路上走的更顺畅。比心。html

译者团队(排名不分前后):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry萝卜vavd317vivaxy萌萌zhouyao前端

JavaScript 轻量级函数式编程

第 11 章:融会贯通

如今你已经掌握了全部须要掌握的关于 JavaScript 轻量级函数式编程的内容。下面不会再引入新的概念。node

本章主要目标是概念的融会贯通。经过研究代码片断,咱们将本书中大部分主要概念联系起来并学以至用。git

建议进行大量深刻的练习来熟悉这些技巧,由于理解本章内容对于未来你在实际编程场景中应用函数式编程原理相当重要。github

准备

咱们来写一个简单的股票行情工具吧。编程

注意: 能够在本书的 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 ..> 元素,并在股票价格更新事件发生时更新价格。bash

在第 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官网:www.ikcamp.com 访问官网更快阅读所有免费分享课程: 《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》 《iKcamp出品|基于Koa2搭建Node.js实战项目教程》 包含:文章、视频、源代码


2019年,iKcamp原创新书《Koa与Node.js开发实战》已在京东、天猫、亚马逊、当当开售啦!