XDM,JS如何函数式编程?看这就够了!(七)

本文涉及的产品
可视分析地图(DataV-Atlas),3 个项目,100M 存储空间
数据可视化DataV,5个大屏 1个月
简介: 如果我们的代码夹杂着命令式风格和声明式风格,即处在 图1.2 红线和绿线的交叉点上的话,那我们代码的可读性或许就处在 图 1.1 虚线和实线的交叉点上,可读性非常低。所以,我们在 JS 函数式编程的路上要更加一往无前!别到最后不伦不类,沦为笑柄 Orz

image.png

引言



继前六篇洗礼之后:



我们可以得出这样的结论:


函数式编程所代表的【声明式代码风格】是高于【命令式代码风格】的!它给了代码可读性数值更多增长的空间!


也可以这样解释:声明式代码风格是基于命令式代码风格,声明式说白了就是对命令式的上层封装!


命令式关注:做什么,申明式关注:是什么


如果我们的代码夹杂着命令式风格和声明式风格,即处在 图1.2 红线和绿线的交叉点上的话,那我们代码的可读性或许就处在 图 1.1 虚线和实线的交叉点上,可读性非常低。所以,我们在 JS 函数式编程的路上要更加一往无前!别到最后不伦不类,沦为笑柄 Orz


第七篇作为系列阶段性的【完结篇】,一切将从实战出发!一切为了实战!

FP will never be slaves.(函数式编程永不为奴)


武器准备



我们在前篇陆续介绍了很多【高级函数】用于实践 JS 函数式编程。

作者将其整理到了这个文件:fp-helpers.js


随便列举几个,都是前面提过的,熟悉的面孔:


function partial(fn,...presetArgs) { // 偏函数
  return function partiallyApplied(...laterArgs){
    return fn( ...presetArgs, ...laterArgs );
  };
}
function curry(fn,arity = fn.length) { // 柯里化
  return (function nextCurried(prevArgs){
    return function curried(nextArg){
      var args = [ ...prevArgs, nextArg ];
      if (args.length >= arity) {
        return fn( ...args );
      }
      else {
        return nextCurried( args );
      }
    };
  })( [] );
}
function compose(...fns) { // 函数组装
    return fns.reduceRight( function reducer(fn1,fn2){
        return function composed(...args){
            return fn2( fn1( ...args ) );
        };
    } );
}
var pipe = reverseArgs(compose); // 函数组装:参数反转,从左到右
function reverseArgs(fn) {
  return function argsReversed(...args){
    return fn( ...args.reverse() );
  };
}
function zip(arr1,arr2) { // 交替选择入参
  var zipped = [];
  arr1 = [...arr1];
  arr2 = [...arr2];
  while (arr1.length > 0 && arr2.length > 0) {
    zipped.push( [ arr1.shift(), arr2.shift() ] );
  }
  return zipped;
}
.....

我们将借用这些高级函数彻底 升级 我们从【命令式编程风格】到【函数式编程风格】!


实践场景



我们用原生模拟一个股票信息操作场景:


DOM 结构:

<ul id="stock-ticker">
    <li class="stock" data-stock-id="AAPL">
        <span class="stock-name">苹果股价</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">微软股价</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">谷歌股价</span>
        <span class="stock-price">$821.31</span>
        <span class="stock-change">-8.84</span>
    </li>
</ul>


格式化数据



一些基本的辅助函数:


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){ // 返回 observable 监听(通过 RxJS,第六篇提过)
    return obsv.map( mapperFn );
}


我们 mock 从服务器获取的数据是这样的:


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


在把 price 的值显示到 DOM 上之前,需要用 formatCurrency(..) 函数格式化一下(比如变成 "$121.70"),同时需要用 formatChange(..) 函数格式化 change 的值(比如变成 "+0.01")。但是我们不希望修改消息对象中的 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(..)操作(参考第 5 篇)。把元组中的信息解构成 propNameval,然后返回了 setProp(..) 调用的结果,这个结果是一个被复制了的新的对象,其中的属性被修改过了。(可理解为深拷贝的进阶版)


我们基于以上再定义几个辅助函数:

var formatDecimal = unboundMethod( "toFixed" )( 2 );
var formatPrice = pipe( formatDecimal, formatCurrency );
var formatChange = pipe( formatDecimal, formatSign );
var processNewStock = pipe( addStockName, formatStockNumbers );
  1. formatDecimal(..) 函数接收一个数字作为参数(如 2.1)并且调用数字的 toFixed( 2 ) 方法。
  2. unboundMethod(..) 函数用来创建一个独立的延迟绑定函数。
  3. formatPrice(..),formatChange(..) 和 processNewStock(..) 都用到了 pipe(..)来从左到右地组合运算。

操作 DOM


下一步,定义一些操作 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" );


这些函数不言自明,从函数命名上就可看出其含义。

以上标出了操作 DOM 元素时的副作用。因为不能简单地用克隆的 DOM 对象去替换,所以勉强接受了一些副作用的产生。但如果在 DOM 渲染中产生一个错误,这样做,我们可以轻松地搜索这些代码注释来缩小可能的错误代码。


查找特定 DOM


现在,我们用 getDOMChildren(..) 实用函数来定义股票行情工具中查找特定 DOM 元素的工具函数:


getDOMChildren(..)listify(..) 来保证我们得到的是一个数组(即使里面只有一个元素)。flatMap(..),这个函数把一个包含数组的数组扁平化,变成一个浅数组。Array.from(..) 把这个数组变成一个真实的数组(而不是一个 NodeList)。

var getDOMChildren = pipe(
    listify,
    flatMap(
        pipe(
            curry( prop )( "childNodes" ),
            Array.from
        )
    )
);
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(..)getStockInfoChildElems(..) 两个实用函数都会过滤掉文字节点,保证返回一个符合股票代码的 DOM 元素数组。


主函数



我们用 stockTickerUI 对象 来保存三个修改界面的主要方法,如下:


var stockTickerUI = {
    updateStockElems(stockInfoChildElemList,data) { // 渲染股票数据到 DOM 元素上
        // ..
    },
    updateStock(tickerElem,data) { // 更新股票数据
        // ..
    },
    addStock(tickerElem,data) { // 添加新股票
        // ..
    }
};


updateStockElems


updateStockElems(..) 内部实现:


var stockTickerUI = {
    updateStockElems(stockInfoChildElemList,data) {
        // **释义1**
        var getDataVal = curry( reverseArgs( prop ), 2 )( data ); 
        // **释义2**
        var extractInfoChildElemVal = pipe(
            getClassName,
            stripPrefix( /\bstock-/i ),
            getDataVal
        );
        // **释义3**
        var orderedDataVals =
            map( extractInfoChildElemVal )( stockInfoChildElemList );
        // **释义4**
        var elemsValsTuples =
            filterOut( function updateValueMissing([infoChildElem,val]){
                return val === undefined;
            } )
            ( zip( stockInfoChildElemList, orderedDataVals ) );
        // **释义5**
        compose( each, spreadArgs )
        ( setDOMContent )
        ( elemsValsTuples );
    },
    // ..
};


释义:

  1. getDataVal(..):首先把 prop 函数的参数反转,柯里化后,把 data 消息对象绑定上去,得到了 getDataVal(..) 函数;
  2. extractInfoChildElemVal(..):接受一个 DOM 元素作为参数,拿到 class 属性的值,然后把 "stock-" 前缀去掉,然后用这个属性值("name","price" 或 "change"),通过 getDataVal(..) 函数,在 data 中找到对应的数据;
  3. orderedDataVals(..):这么做的目的是按照 stockInfoChildElemList 中的 <span> 元素的顺序从 data 中拿到数据。我们对 stockInfoChildElemList 数组调用 extractInfoChildElem 映射函数,来拿到这些数据;
  4. elemsValsTuples(..):我们用来过滤掉数据对象中值为空的元组,筛选后的结果是一个元组数组(zip压缩:[ <span>, ".." ]);
  5. 最后调用,我们更新 DOM 中的 元素:


updateStock


updateStock(..),是三个函数里面最简单的:


var stockTickerUI = {
    // ..
    updateStock(tickerElem,data) {
        var getStockElemFromId = curry( getStockElem )( tickerElem );
        var stockInfoChildElemList = pipe(
            getStockElemFromId,
            getStockInfoChildElems
        )
        ( data.id );
        return stockTickerUI.updateStockElems(
            stockInfoChildElemList,
            data
        );
    },
    // ..
};
  1. 柯里化之前的辅助函数 getStockElem(..),传给它 tickerElem,得到了 getStockElemFromId(..) 函数,这个函数接受 data.id 作为参数。
  2. 把 <li> 元素(其实是数组形式的)传入 getStockInfoChildElems(..),我们得到了三个 子元素,用来展示股票信息,我们把它们保存在 stockInfoChildElemList 变量中。
  3. 然后把数组和股票信息 data 对象一起传给 stockTickerUI.updateStockElems(..),来更新 中的数据。


addStock


addStock(..),会根据新的股票信息生成一个空的 DOM 结构,然后调用 stockTickerUI.updateStockElems(..) 方法来更新其中的内容。


var stockTickerUI = {
    // ..
    addStock(tickerElem,data) {
        // **释义1**
        var [stockElem, ...infoChildElems] = map(
            createElement
        )
        ( [ "li", "span", "span", "span" ] );
        // **释义2**
        var attrValTuples = [            [ ["class","stock"], ["data-stock-id",data.id] ],
            [ ["class","stock-name"] ],
            [ ["class","stock-price"] ],
            [ ["class","stock-change"] ]
        ];
        // **释义3**
        var elemsAttrsTuples =
            zip( [stockElem, ...infoChildElems], attrValTuples );
        // **释义4**
        // 副作用!!
        each( function setElemAttrs([elem,attrValTupleList]){
            each(
                spreadArgs( partial( setElemAttr, elem ) )
            )
            ( attrValTupleList );
        } )
        ( elemsAttrsTuples );
        // **释义5**
        // 副作用!!
        stockTickerUI.updateStockElems( infoChildElems, data );
        reduce( appendDOMChild )( stockElem )( infoChildElems );
        tickerElem.appendChild( stockElem );
    }
};


释义:

  1. 我们先创建 <li> 父元素和三个 <span> 子元素,把它们分别赋值给了 stockEleminfoChildElems 数组;
  2. 为了设置 DOM 元素的对应属性,我们声明了一个元组数组组成的数组。按照顺序,每个元组数组对应上面四个 DOM 元素中的一个。每个元组数组中的元组由对应元素的属性和值组成:
  3. 我们把四个 DOM 元素和 attrValTuples 数组 zip(..) 起来;
  4. 把属性和值设置到每个 DOM 元素上;
  5. 调用;


实现结果是:

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


到此为止,我们有了 <span> 元素数组,每个元素上都有了该有的属性,但是还没有 innerHTML 的内容。这里,我们要用 stockTickerUI.updateStockElems(..) 函数,把 data 设置到 <span> 上去,和股票信息更新事件的处理一样。


小结


以上,我们模拟了股票数据操作场景下是如何进行函数式编程的!可能读起来有些晦涩,但是老话说的好:书读百遍,其意自现。 看多了,看久了,也自然能感知一二!

如果你一直以来惯用命令式编程,可以设想下,在命令式编程中,它们会怎么去实现的?


其实,还有一个很重要的点,上面没写,即实现 observable 订阅功能,把事件传递给主函数。


所以,这里的范例更推荐去往项目【ch11-code】 整体阅读!

此篇示例更多的是把前篇概念联系起来,提供更真实的例子来学习。


在你豁然开朗以前一定要持续不断地练习!!


函数库



至此,你应该已经明白:上面提到的辅助函数(高级函数)的确很厉害,但是我自己写不出来,怎么办呢?


没关系,使用武器的人不需要知道武器是怎么生产出来的!


没错,我们先要学会使用这些函数式编程中强大的第三方武器库!


然后再去谈,善用武器的人都应该知道武器原理 Balabala~

它们是:

更多关于 Monad,挖个坑,后面填。


总结



本阶段关于函数式编程已经说了这么多了~最后的最后,来点形而上学,高屋建瓴。

我不需要再为各位开发者想出更多崇高的理由来激励大家前行。感谢大家一起参与学习 JavaScript 中的函数式编程。期望你我充满希望!—— cognitect-lab


当你在绝望和沮丧的低谷时,别停下来。前面等待你的是一种更好的思维方式!

可以预见的是,本瓜后期还会多次针对 JS 的函数式编程进行更多探究和分享!

感谢阅读!


掘金安东尼,输出暴露输入,技术洞见生活!再会~


相关实践学习
Github实时数据分析与可视化
基于Github Archive公开数据集,将项目、行为等20+种事件类型数据实时采集至Hologres进行分析,并搭建可视化大屏。
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
10月前
|
前端开发 JavaScript 数据处理
深入学习JavaScript ES8函数式编程:特性与实践指南
深入学习JavaScript ES8函数式编程:特性与实践指南
68 0
|
2月前
|
存储 JavaScript 前端开发
JavaScript——函数式编程Functor(函子)
JavaScript——函数式编程Functor(函子)
14 0
|
4月前
|
前端开发 JavaScript 开发者
函数式编程在JavaScript中的应用
【6月更文挑战第10天】本文探讨了函数式编程在JavaScript中的应用,介绍了函数式编程的基本概念,如纯函数和不可变数据。文中通过示例展示了高阶函数、不可变数据的使用,以及如何编写纯函数。此外,还讨论了函数组合和柯里化技术,它们能提升代码的灵活性和可重用性。掌握这些函数式编程技术能帮助开发者编写更简洁、可预测的JavaScript代码。
|
5月前
|
JavaScript 前端开发
JavaScript 的数组方法 map()、filter() 和 reduce() 提供了函数式编程处理元素的方式
【5月更文挑战第11天】JavaScript 的数组方法 map()、filter() 和 reduce() 提供了函数式编程处理元素的方式。map() 用于创建新数组,其中元素是原数组元素经过指定函数转换后的结果;filter() 则筛选出通过特定条件的元素生成新数组;reduce() 将数组元素累计为单一值。这三个方法使代码更简洁易读,例如:map() 可用于数组元素乘以 2,filter() 用于选取偶数,reduce() 计算数组元素之和。
42 2
|
5月前
|
JavaScript 前端开发 测试技术
JavaScript中的函数式编程:纯函数与高阶函数的概念解析
【4月更文挑战第22天】了解JavaScript中的函数式编程,关键在于纯函数和高阶函数。纯函数有确定输出和无副作用,利于预测、测试和维护。例如,`add(a, b)`函数即为纯函数。高阶函数接受或返回函数,用于抽象、复用和组合,如`map`、`filter`。函数式编程能提升代码可读性、可维护性和测试性,帮助构建高效应用。
|
5月前
|
前端开发 JavaScript 数据处理
深入学习JavaScript ES8函数式编程:特性与实践指南
深入学习JavaScript ES8函数式编程:特性与实践指南
107 0
|
5月前
|
JavaScript 前端开发 索引
JavaScript函数式编程【进阶】
JavaScript函数式编程【进阶】
47 1
|
5月前
|
存储 JavaScript 前端开发
JavaScript函数式编程[入门]
JavaScript函数式编程[入门]
40 1
|
12月前
|
缓存 JavaScript 前端开发
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(1)
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(1)
|
12月前
|
JavaScript 前端开发 测试技术
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(2)
带你读《现代Javascript高级教程》十四、JavaScript函数式编程(2)
下一篇
无影云桌面