轻量函数式 JavaScript:十一、综合应用

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

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

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

准备

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

注意: 为了便于引用,这个示例的全部代码位于 ch11-code/ 子目录中 —— 参见这本书的 GitHub 代码库 (https://github.com/getify/Functional-Light-JS)。另外,这个示例需要选用一些我们在这本书中讨论过的 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}`;
}
function transformObservable(mapperFn,obsv){
    return obsv.map( mapperFn );
}

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

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

transformObservable(..) 上有一个微妙的地方需要注意:因为在一个 observable 上进行 map(..) 返回一个新的 observable,所以它看起来是纯粹的。但从技术上讲,在它的底层,obsv 的内部状态被改变为连接到 map(..) 返回的新 observable。这种副作用没什么大不了的,而且不会损害我们代码的可读性,但是无论副作用在什么地方你都能发现它们非常重要,而不是在你遇到 bug 时被它们吓一跳。

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

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

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

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 保持两个属性名和格式化后的值的元组(就是数组)。我们在这个数组上 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 [ newStocks, stockUpdates ] = pipe(
    map( makeObservableFromEvent ),
    curry( zip )( observableMapperFns ),
    map( spreadArgs( transformObservable ) )
)
( [ "stock", "stock-update" ] );

我们从事件名称的数组开始 (["stock","stock-update"]),将这个列表 map(..)(参见第八章)为一个包含两个 observable 的列表,然后将这个列表 zip(..)(参见第八章)为一个 observable 映射函数的列表;这个映射生成了一个元组的列表,就像 [ observable, mapperFn ]。最后,我们使用 transformObservable(..)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(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( .. ))(参见第三章)而非 partialRight(..),仅仅是为了这种特定的情况挤出好一些的性能。

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

matchingStockId(..) 展示了闭包(参见第二章)的用法(之一!)—— 创建一个即使稍后在一个不同作用域中运行时也能记住变量 id 的内部函数(isStock(..))。

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

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

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

现在,让我们使用这个 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 元素开始,取得它的子元素,然后过滤它来确保我们得到匹配指定证券标识符的元素。getStockInfoChildElems(..) 做的几乎是相同的事情,除了它是从一个证券元素开始,而且使用不同的过滤器来过滤。

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

主 API

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

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
        );
    },

    // ..

};

使用 tickerElem 柯里化早先的帮助函数 getStockElem(..) 给了我们 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 );
    },

    // ..

};

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

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

接下来让我们看看 extractInfoChildElem

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

这个函数接收一个 DOM 元素,取得它的 DOM class,截去 "stock-" 前缀,然后使用这个值("name""price"、或 "change")通过 getDataVal(..)data 中抽取同名属性的值。表面上看,这种行为可能有些奇怪。

它的目的是按照与 <span> 元素(在 stockInfoChildElemList 中)相同的顺序从 data 中抽取值。我们通过将 extractInfoChildElem(..) 用作这个列表的映射函数来完成这个任务,将其结果列表称为 orderedDataVals

下面,我们将把 <span> 列表和值的列表 zip 起来,生成一些元组:

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(..)。我们先整体地定义它,然后像之前一样一步一步地检视它:

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 );
    }

};

这个 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 上:

tickerElem.appendChild( 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(
    curry( reverseArgs( partial ), 2 )( ticker )
)
( [ stockTickerUI.addStock, stockTickerUI.updateStock ] );

和之前一样,reverseArgs( partial )partialRight(..) 性能优化后的替代品。但是这次,partial(..) 是我们要使用的映射函数。为此,我们需要 curry(..) 它以便提前制定第二个参数 ticker;当每个 UI 方法之后被映射时,它会使用 ticker 对这个函数进行局部应用。现在,这两个在结果数组中的双重局部应用函数可以用于订阅 observable 了。

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

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

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

unboundMethod("subscribe") 是自动被柯里化的,所以我们 uncurry(..) 它(参见第三章),然后使用 spreadArgs(..) 适配它(同样参照第三章),这样它将把一个元组数组扩散为它的两个参数。

现在,我们只需要一个 observable 的列表,这样我们就可以将它与封装了上下文环境的 UI 方法列表 zip(..) 在一起。之后这个元组的列表的每一个都可以使用我们刚刚在前一个代码段中定义的 subscribeToObservable(..) 进行订阅:

var stockTickerObservables = [ newStocks, stockUpdates ];

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

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

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

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

总结

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

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

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

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

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

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

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

时间: 2024-11-02 09:27:55

轻量函数式 JavaScript:十一、综合应用的相关文章

轻量函数式 JavaScript:一、为什么要进行函数式编程?

函数式程序员:(名词)那些将变量命名为"x",函数命名为"f",并将代码模式称为"zygohistomorphic prepromorphism"的程序员. James Iry ‏@jamesiry 5/13/15 https://twitter.com/jamesiry/status/598547781515485184 函数式编程(FP)无论如何都不是一个新概念.它在编程的整个历史中一直存在着.然而可以确定的是 -- 我不确信这样说是否合理,

轻量函数式 JavaScript:七、闭包 vs 对象

多年以前,Anton van Straaten 编写了一个名声显赫而且广为流传的 禅家公案,描绘并挑起了闭包与对象之间一种重要的紧张状态. 庄严的 Qc Na 大师在与他的学生 Anton 一起散步.Anto 希望促成一次与师傅的讨论,他说:"师傅,我听说对象是个非常好的东西 -- 真的吗?" Qc Na 同情地看着他的学生回答道,"笨学生 -- 对象只不过是一种简单的闭包." 被训斥的 Anton 告别他的师父返回自己的房间,开始有意地学习闭包.他仔细地阅读了整部

轻量函数式 JavaScript:附录 A、Transducing

与我们在本书中所讲解的内容相比,Transducing 是一种更高级的技术.它扩展了第八章中的列表操作的许多概念. 我不认为这个话题是严格的 "轻量函数式",它更像是在此之上的额外奖励.我将它留作附录是因为你很可能需要暂且跳过关于它的讨论,而在你对本书正文中的概念感到相当适应 -- 并且确实经过实践! -- 之后再回到这里. 老实说,即便是教授了 transducing 许多次,而且编写了这一章之后,我依然在努力地尝试使用这种技术来武装自己的头脑.所以,如果它让你感到很绕也不要灰心.给

轻量函数式 JavaScript:十、函数式异步

这本书读到这里,你现在拥有了所有 FP -- 我称之为 "轻量函数式编程" -- 基础的原始概念.在这一章中,我们会将这些概念应用于一种不同的环境,但不会出现特别的新想法. 至此,我们做的所有事情几乎都是同步的,也就是说我们使用立即的输入调用函数并立即得到输出值.许多工作可以用这种方式完成,但对于一个现代 JS 应用程序的整体来说根本不够用.为了真正地对 JS 的现实世界中的 FP 做好准备,我们需要理解异步 FP. 我们本章的目标是将我们对使用 FP 进行值的管理的思考,扩展至将这样

轻量函数式 JavaScript:二、函数式函数的基础

函数式编程 不是使用 function 关键字编程. 如果它真有那么简单,我在这里就可以结束这本书了!但重要的是,函数确实是 FP 的中心.使我们的代码成为 函数式 的,是我们如何使用函数. 但是,你确信你知道 函数 是什么意思吗? 在这一章中,我们将要通过讲解函数的所有基本方面来为本书的剩余部分打下基础.在某种意义上,这里的内容即便是对非 FP 程序员来说也是应当知道的关于函数的一切.但是如果我们想要从 FP 的概念中学到竟可能多的东西,我们必须 知道 函数的里里外外. 振作起来,关于函数东西

轻量函数式 JavaScript:五、降低副作用

在第二章中,我们讨论了一个函数如何能够拥有 return 值之外的输出.至此你应当对一个函数的 FP 定义感到非常适应了,那么这种副输出 -- 副作用!-- 的想法应当散发出臭味了. 我们将要检视各种不同形式的副作用,并看看为什么它们对我们代码的质量和可读性有害. 但别让我在这里喧宾夺主.这一章的要点是:写出一个没有副作用的程序是不可能的.好吧,不是不可能:你当然能.但是那样的程序将不会有什么用,也无法观察.如果你写了一个副作用为零的程序,那么你将无法说出它与一个被删除的或空的程序有什么区别.

轻量函数式 JavaScript:八、列表操作

你在前一章闭包/对象的兔子洞中玩儿的开心吗?欢迎回来! 如果你能做很赞的事情,那就反复做. 我们在本书先前的部分已经看到了对一些工具的简要引用,现在我们要非常仔细地看看它们,它们是 map(..).filter(..).和 reduce(..).在 JavaScript 中,这些工具经常作为数组(也就是"列表")原型上的方法使用,所以我们很自然地称它们为数组或列表操作. 在我们讨论具体的数组方法之前,我们要在概念上检视一下这些操作是用来做什么的.在这一章中有两件同等重要的事情:理解列表

轻量函数式 JavaScript:三、管理函数输入

在第二章的"函数输入"中,我们谈到了函数形式参数与实际参数的基础.我们还看了一些语法技巧来方便它们的使用,比如 ... 操作符和解构. 我在讨论中建议你应当尽可将函数设计为只含一个形式参数.事实上,这不总是可能的,而且你也不总是能够控制你使用的函数的签名. 现在我们将要把注意力转向在这些场景下操纵函数输入的更精巧.更强大的模式. 有些现在,有些后来 如果一个函数接收多个实际参数,你可能想要提前指定其中的一些,而在稍后指定其余的. 考虑这个函数: function ajax(url,da

轻量函数式 JavaScript:九、递归

在下一页,我们将进入递归的话题. (本页的剩余部分故意被留作空白)                       让我们来谈谈递归.在深入之前,参见前一页来了解其正式的定义. 很弱的玩笑,我知道.:) 递归是那些大多数开发者都承认其非常强大,但同时也不喜欢使用的技术之一.在这种意义上,我将之与正则表达式归为同一范畴.强大,但令人糊涂,因此看起来 不值得花那么多力气. 我是一个递归的狂热爱好者,而且你也可以是!我在本章的目标就是说服你,递归是一种你应当带入到你 FP 编码方式中的重要工具.当使用得当