Skip to content

Latest commit

 

History

History
542 lines (400 loc) · 27 KB

ch11.md

File metadata and controls

542 lines (400 loc) · 27 KB

轻量函数式 JavaScript

第十一章:综合应用

现在,你拥有了理解轻量函数式 JavaScript 所需的一切。再没有新的概念要介绍了。

在这最后的一章中,我们的目标是凝聚这些概念。我们将看到将这本书中的许多主题融合在一起的代码 —— 应用我们学到的东西。最重要的是,这篇代码示例是为了展示 “轻量函数式” 应用到 JavaScript 上的方式 —— 也就是,教条之上的平衡以及实用主义。

你将会想要广泛地亲自实践这些技术。消化理解这一章对于你将 FP 的概念应用于真实世界的代码至关重要。

准备

让我们建造一个简单的证券报价机控件。

注意: 为了便于引用,这个示例的全部代码位于 ch11-code/ 子目录中 —— 参见本书的 GitHub 代码库。另外,这个示例需要选用一些我们在这本书中讨论过的 FP 帮助函数,它们包含在 ch11-code/fp-helpers.js 中。本章中我们仅将注意力集中在与我们的讨论有关的部分代码中。

首先,让我谈谈这个控件的置标代码,这样我们才有地方来展示信息。我们从 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 的交互是一种 I/O,而这意味着副作用。我们不能消灭这些副作用,但可以限制并控制它们。我们要确实有意地将我们的应用程序处理 DOM 的表面积控制在最小。我们已经在第五章中学习过这些技术了。

概括一下我们控件的功能:这段代码将会在每次 “收到” 新证券事件时添加一个 <li ..> 元素,并在证券更新事件到来时更新它的价格。

在第十一章的示例代码,ch11-code/mock-server.js 中,我们设立了一些定时器来随机地向一个简单事件发生器推送虚构的证券数据,以模拟我们正在从一个服务器接受证券信息。我们暴露了一个 connectToServer() 函数假装这样做,但实际上只是返回一个虚构的事件发生器实例。

注意: 这个文件中都是虚构/模拟的行为,所以我没有花费太多的力气来使它支持 FP。我不建议你花太多的时间来关心这个文件中的代码。如果你写了一个真的服务器 —— 对于有雄心的读者来说是一个非常有趣的额外练习! —— 那么显然你将会对这段代码进行它应得的 FP 思考。

ch11-code/stock-ticker-events.js 中,我们(通过 RxJS)建立了某种连接到事件发生器对象的 observable。我们调用 connectToServer() 来得到这个事件发生器,然后监听名为 "stock"(向我们的报价机添加新证券)和 "stock-update"(更新证券的价格以及涨跌额度)的事件。最后,我们为这些 observable 的输入数据定义变形规则,按需要格式化数据。

ch11-code/stock-ticker.js 中,我们在 stockTickerUI 对象上将 UI(DOM 副作用)的行为定义为方法。我们还定义了各种帮助函数,包括 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}`;
}

这些纯函数理解起来应该相当直接。回忆一下,第四章中的 setProp(..) 在设置新属性之前实际上克隆了对象。这践行了我们在第六章中看到的原则:通过将值视为不可变的 —— 即使它们不是 —— 来避免副作用。

addStockName(..) 用来向一个证券消息对象添加 name 属性,值与它的 id 相等。name 的值稍后用作控件中可见的证券名称。

当一个来自于 “服务器” 的证券信息被接受到时,它看起来就像这样:

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

在展示在 DOM 上之前,price 需要用 formatCurrency(..) 进行格式化(为了看起来像 "$121.70"),而且 change 需要用 formatChange(..) 进行格式化 (为了看起来像 "+0.01")。但我们不想改变消息对象,所以我们需要一个帮助函数来格式化这两个数字并给我们一个新的证券对象;

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

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

我们创建了 stockDataUpdates 数组来分别为 pricechange 保持两个属性名和格式化后的值的元组(就是数组)。我们在这个数组上 reduce(..)(见第九章),将 stock 对象作为 initialValue。我们将元组分解为 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 ) 方法。我们使用第九章的 unboundMethod(..) 来建立一个独立的推迟绑定的方法。

formatPrice(..)formatChange(..)、和 processNewStock(..) 都使用 pipe(..) 将一些操作从左至右地组合起来(见第四章)。

为了从我们的事件发生器中创建 observable(见第十章),我们需要一个帮助函数,它是 RxJS 中 Rx.Observable.fromEvent(..) 的一个柯里化(见第三章)独立函数:

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

这个函数被指定为监听 server(事件发生器),而且在等待一个事件名称字符串来生成它的 observable。现在我们准备好了为两个事件创建 observer 所需的所有配件,可以对这些 observer 进行映射变形来格式化输入数据了:

var observableMapperFns = [ processNewStock, formatStockNumbers ];

var stockEventNames = [ "stock", "stock-update" ];

var [ newStocks, stockUpdates ] = pipe(
    map( makeObservableFromEvent ),
    curry( zip )( observableMapperFns ),
    map( spreadArgs( mapObservable ) )
)
( stockEventNames );

我们从事件名称的数组 stockEventNames 开始 (["stock","stock-update"]),将这个列表 map(..)(见第九章)为一个包含两个 observable 的列表,然后将这个列表 zip(..)(见第九章)为一个 observable 与映射函数的列表;这个映射生成了一个元组的列表,就像 [ observable, mapperFn ]。最后,我们使用 mapObservable(..)map(..) 这个元组的列表,并使用 spreadArgs(..)(见第三章)来把每个元组分散为独立的参数。

结果就是一个包含两个被映射的 observable 列表,我们通过数组解构将它们分别赋值给 newStocksstockUpdates

就是这样;这就是我们如何使用轻量 FP 方式建立证券报价事件 observable!我们将在 ch11-code/stock-ticker.js 中订阅这两个 observable。

退后一步并反思一下我们在这里对 FP 原理的使用。它合理吗?你能看出我们是如何应用这本书前几章中讲解的各种概念的吗?你能想出完成这些任务的其他方法吗?

更重要的是,你如何用指令式方式完成它,而且你对这两种方式大体上比较起来有什么看法?试着练习一下。使用你熟知的指令式方式编写它的等价物。如果你像我一样,指令式形式将依然使人感觉更自然。

在继续之前你需要 学会 的是,你 可以理解并推理我们刚刚展示的 FP 风格。考虑一下每一个函数和代码段的形状(输入与输出)。你能看出它们是如何联系在一起的吗?

在你适应这些东西之前,不断练习。

证券报价机的 UI

如果你对前一节中的 FP 感到相当舒适,那么你就准备好深入 ch11-code/stock-ticker.js 了。它相当复杂,所以我们将花一些时间来看看它整体的每一个部分。

让我们先定义一些可以辅助我们 DOM 操作任务的帮助函数:

function isTextNode(node) {
    return node && node.nodeType == 3;
}
function getElemAttr(prop,elem) {
    return elem.getAttribute( prop );
}
function setElemAttr(elem,prop,val) {
    // !!副作用!!
    return elem.setAttribute( prop, val );
}
function matchingStockId(id,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( getElemAttr, 2 );
var getStockId = getElemAttrByName( "data-stock-id" );
var getClassName = getElemAttrByName( "class" );
var isMatchingStock = curry( matchingStockId, 2 );

这些东西几乎都应该是自解释的。

注意我明确指出了改变 DOM 元素状态的副作用。我们无法很容易地克隆一个 DOM 对象并替换它,所以在这里我们安于一些改变既存 DOM 元素的副作用。至少,如果我们在 DOM 的渲染上发生了 bug,我们可以很容易地检索这些代码注释来缩小可疑代码的范围。

这是一些其他的杂项帮助函数:

function stripPrefix(prefixRegex,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(..) 来确保我们有一个元素的列表(即便它只有一个元素)。回忆一下第九章的 flatMap(..),它映射一个列表并将一个列表的列表扁平化为一个浅层列表。

我们这里的映射函数将一个元素映射为它的 childNodes 列表,然后我们使用 Array.from(..) 将它变成真正的数组(而不是一个实时的 NodeList)。这两个函数(通过 pipe(..))被组合为一个单独的映射函数,这就是融合(见第九章)。

现在,让我们使用这个 getDOMChildren(..) 帮助函数来定义从控件中取得指定 DOM 元素的工具:

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

getStockElem(..) 从我们控件的 tickerElem DOM 元素开始,取得它的子元素,然后过滤它来确保我们得到匹配指定证券标识符的元素。getStockInfoChildElems(..) 做的几乎是相同的事情,除了它是从一个证券元素开始,而且使用不同的过滤器来过滤。

两个工具都滤除了文本节点(因为它们与真正的 DOM 节点的工作方式不同),而且两个工具都返回一个 DOM 元素的数组,即使它仅含有一个元素。

主 API

我们将使用一个 stockTickerUI 对象来组织我们的三个主 UI 操作方法,就像这样:

var stockTickerUI = {

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

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

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

让我们首先检视一下 updateStock(..),因为它是三个中最简单的:

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

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

使用 tickerElem 柯里化早先的帮助函数 getStockElem(..) 给了我们 getStockElemFromId(..),它将接收 data.id

通过 pipe(..)getStockElemFromId(data.id) 的返回值是一个 <li> 元素(实际上,是一个仅含此元素的列表),它被传递给 getStockInfoChildElems(..)

结果是一个用于显示证券信息的列表(stockInfoChildElemList),它包含三个 <span> 元素。为了使用新数据实际更新这三个 <span> 元素,我们将这个列表和证券的 data 消息对象传递给 stockTickerUI.updateStockElems(..)

现在让我们看看 stockTickerUI.updateStockElems(..) 是如何定义的:

updateStockElems(stockInfoChildElemList,data) {
    var getDataVal = curry( reverseArgs( prop ), 2 )( data );
    var extractInfoChildElemVal = pipe(
        getClassName,
        curry( 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 );
},

我知道,信息量有点儿大。我们一个语句一个语句地分解它。

getDataVal(..) 被反转参数顺序后柯里化,再绑定掉 data 消息对象上,现在它在等待一个属性名以便从 data 中进行抽取。

接下来让我们看看 extractInfoChildElem 是如何定义的:

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

这个函数接收一个 DOM 元素,取得它的 DOM class,截去 "stock-" 前缀,然后使用这个值("name""price"、或 "change")通过 getDataVal(..)data 对象中抽取同名属性的值。

这种从 data 对象中抽取值的方法看起来似乎再绕圈子。它的目的是按照与 <span> 元素在 stockInfoChildElemList 中出现的相同顺序从 data 中抽取值。我们通过将 extractInfoChildElem(..) 用作这个列表的映射函数来完成这个任务,将其结果列表称为 orderedDataVals

下面,我们将把 <span> 列表和有顺序的值的列表 zip 起来,生成一些元组。在这些元组中 DOM 元素和用来更新它的值成对出现:

zip( stockInfoChildElemList, orderedDataVals )

由于我们定义 observable 变形的方式,这里有一个微妙有趣的小问题,新的证券消息对象在 data 中有一个 name 属性可以与 <span class="stock-name"> 元素相匹配,但是在更新证券用的消息对象上 name 就不存在。

如果数据消息对象没有一个属性,我们就不应该更新相应的 DOM 元素。所以,我们需要 filterOut(..) 所有第二个位置(在这个例子中,是数据值)为 undefined 的元组:

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

这个过滤处理的结果是一个准备用于更新 DOM 内容的元组列表(就像 [ <span>, ".." ]),我们将它赋值给 elemsValsTuples

注意: 因为判定函数 updateValueMissing(..) 在这里被内联地指定,所以我们可以控制它的签名。与使用 spreadArgs(..) 来适配它,以便将一个数组的实际参数扩散为两个单独的命名形式参数不同,我们在函数声明中使用了形式参数数组解构(function updateValueMissing([infoChildElem,val]){ ..);更多信息可以参见第二章

最后,我们需要更新 <span> 元素的 DOM 内容:

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

我们使用 each(..)(参见第九章中 forEach(..) 的讨论)迭代这个 elemsValsTuples 列表。

与其他地方使用的 pipe(..) 不同,这个组合使用 compose(..)(见第四章)来将 setDomContent(..) 传入 spreadArgs(..),然后其结果作为迭代函数被传递给 each(..)。每一个元组都被扩散为 setDOMContent(..) 的参数,之后由它更新相应的 DOM 元素。

解决了两个主 UI 方法,还有一个:addStock(..)。我们先整体地定义它,然后像之前一样一步一步地检视它:

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 );
    appendDOMChild( tickerElem, stockElem );
}

这个 UI 方法需要为新的证券元素创建空的 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"] ]
];

现在我们要将四个元素的列表与这个 attrValTuples 列表 zip(..) 起来:

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

最后列表的结构看起来将是这样:

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

如果我们想要指令式地处理这种数据结构,将属性-值元组赋值到每个 DOM 元素的话,我们可能要使用嵌套的 for 循环。我们的 FP 方式也类似,不过使用的是嵌套的 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(..)(见第九章)来这样做:

reduce( appendDOMChild )( stockElem )( infoChildElems );

最后,用一个老式的 DOM 变更副作用将新的证券元组追加到控件的 DOM 上:

appendDOMChild( tickerElem, stockElem );

咻!你都跟上了?在前进之前,我建议你回过头去将这次讨论重读几次并实践一下代码。

订阅 Observable

我们最后的主要任务是订阅定义在 ch11-code/stock-ticker-events.js 中的 observable,并将这些订阅内容连接在恰当的主 UI 方法(addStock(..)updateStock(..))上。

首先,我们注意到这些方法每个都期待 tickerElem 作为第一个参数。我们来制造一个列表(stockTickerUIMethodsWithDOMContext),它通过局部应用(也就是闭包;参见第二章)将 ticker 控件的 DOM 元素与这两个方法封装起来:

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

var stockTickerUIMethodsWithDOMContext = map(
    pipe( partialRight, unary )( partial, ticker )
)
( [ stockTickerUI.addStock, stockTickerUI.updateStock ] );

首先,我们在 partial(..) 工具上使用 partialRight(..)(右侧局部应用),将它最右侧的参数预设为 ticker。然后我们通过 pipe(..) 把这个被右侧局部应用过的 partial(..) 传递给 unary(..) 以防止它从 map(..) 那里收到不合时宜的额外参数(见第三章)。其结果是一个映射函数,它期待一个函数来(用一个参数:ticker)局部应用。我们使用这个映射函数来分别 map(..) stockTickerUI.addStock(..)stockTickerUI.updateStock(..) 函数。

map(..) 的结果是一个数组 stockTickerUIMethodsWithDOMContext,它持有两个局部应用过的函数;现在,这两个函数可以用于订阅 observable 了。

虽然我们使用了闭包来将 ticker 的状态保留在这两个函数中,但是在第七章中我们看到了可以将这个 ticker 值作为一个对象上的属性 “保留” 下来,也许是通过用 this 将每个函数绑定到 stockTickerUI。因为 this 是一种隐含的输入(见第二章),而且这通常来说不太好,所以我选择了闭包而非对象。

为了订阅 observable,我们来制造一个用于解除绑定方法的帮助函数:

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

unboundMethod("subscribe") 是被柯里化的,所以我们 uncurry(..) 它(见第三章)。

现在,我们只需要一个 observable 的列表,这样我们就可以将它与绑定了 DOM 的 UI 方法列表 zip(..) 在一起;得出的元组将同时包含 observable 与订阅它的监听器函数。我们用 each(..) 处理每个元组并使用 spreadArgs(..) (见第三章)将元组的内容扩散为 subscribeToObservable(..) 的两个参数:

var stockTickerObservables = [ newStocks, stockUpdates ];

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

因为从技术上讲,为了订阅这些 observable,我们在改变它们的状态,而且也因为我们在使用 each(..) —— 几乎总是与副作用相关! —— 所以我们在代码注释中指出这个事实。

就是这样!就像早先我们在讨论证券报价机事件时做的那样,花同样的时间重复阅读并将这段代码与它的指令式形式进行比较吧。真的,花些时间来做。我知道你已经读了很多东西,但你在这本书中跋涉这么久说到底是为了能够消化并理解这样的代码。

你对在 JavaScript 中以一种平衡的方式使用 FP 感觉怎么样?就像我们在这里做的一样,继续练习吧!

总结

我们在本章中讨论的代码应当作为整体来看待,而不只是像在本章中一样被打碎成片段。现在就停下去通读完整的文件吧。确保你能在完整的上下文中理解它们。

这个代码示例不是要成为你应当如何确切编写代码的规定。它是想要更好地描述如何使用轻量 FP 技术来思考并着手处理这样的任务。它想要尽可能多地将本书中的不同概念关联起来。它想要在更 “真实” 的代码场景下探索 FP,而非我们通常引用的代码片段。

我十分确信我在自己的旅程中更好地理解了 FP,我将会继续改进自己编写这段实例代码的方式。你现在看到的只是我的弧线上的一个快照。我希望这对你也一样。

在我们结束本书正文之前,我想提醒你的是我在第一章中分享过的这幅可读性曲线:

在这次学习,以及在你的 JavaScript 中应用 FP 原理的旅途中,将这幅图牢记在心并为你自己设定现实的目标是十分重要的。你已经坚持到了这里,这是十分了不起的成就。

但是当你向着绝望和失落的低谷倾斜的时候不要放弃。在另一边等着你的是一种对你代码的思考和交流的方式,它更易于解读,易于理解,易于验证,而最终,更加可靠。

对于我们开发者来说,我想不出值得为之努力的更高尚的目标。感谢你与我分享在 JavaScript 中学习 FP 原理的旅途。希望你的旅途和我的一样丰富多彩!