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"
)。但是我们不希望修改消息对象中的 price
和 change
,所以我们需要一个辅助函数来格式化这些数字,并且要求这个辅助函数返回一个新的消息对象,其中包含格式化好的 price
和 change
:
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
元组来保存 price
和 change
的信息,包括属性名称和格式化好的值。把 stock
对象作为 initialValue
,对元组进行 reduce(..)
(参考第 8 章)。把元组中的信息解构成 propName
和 val
,然后返回了 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 的数组,通过数组结构赋值的方式分别赋值到了 newStocks
和 stockUpdates
两个变量上。
到此为止,我们用轻量级函数式编程的方式来让股票行情信息事件成为了 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>
子元素,把它们分别赋值给了 stockElem
和 infoChildElems
数组。
为了设置 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
保存在对象的属性上,通过使用两个函数上的指向 stockTickerUI
的 this
来访问 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 中的函数式编程的原理的过程中来。我希望你的学习过程和我的一样,充实而充满希望!