JS轻量级函数式编程实现XDM三


Posted in Javascript onJune 16, 2022

前言

这是【JS如何函数式编程】系列文章第三篇。点赞?关注?,持续追踪?

前两篇传送门:

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

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

在第二篇,我们谈了基础之基础,重要之重要——“偏函数”,偏函数通过函数封装,实现了减少传参数量的目的,解决了手动指定实参的麻烦。

更具重要意义的是:

当函数只有一个形参时,我们能够比较容易地组合它们。这种单元函数,便于进行后续的组合函数;

没错,本篇就是谈关于 “组合函数”。它是函数编程的重中之重之重之重重重!

组合函数

含义

函数编程就像拼乐高!

乐高有各式各样的零部件,我们将它们组装拼接,拼成一个更大的组件或模型。

函数编程也有各种功能的函数,我们将它们组装拼接,用于实现某个特定的功能。

下面来看一个例子,比如我们要使用这两个函数来分析文本字符串:

function words(str) {
    return String( str )
        .toLowerCase()
        .split( /\s|\b/ )
        .filter( function alpha(v){
            return /^[\w]+$/.test( v );
        } );
}
function unique(list) {
    var uniqList = [];
    for (let i = 0; i < list.length; i++) {
        if (uniqList.indexOf( list[i] ) === -1 ) {
            uniqList.push( list[i] );
        }
    }
    return uniqList;
}
var text = "To compose two functions together";
var wordsFound = words( text );
var wordsUsed = unique( wordsFound );
wordsUsed;
//  ["to", "compose", "two", "functions", "together"]

不用细看,只用知道:我们先用 words 函数处理了 text,然后用 unique 函数处理了上一处理的结果 wordsFound;

这样的过程就好比生产线上加工商品,流水线加工。

JS轻量级函数式编程实现XDM三

想象一下,如果你是工厂老板,还会怎样优化流程、节约成本?

这里作者给了一种解决方式:去掉传送带!

JS轻量级函数式编程实现XDM三

即减少中间变量,我们可以这样调用:

var wordsUsed = unique( words( text ) );
wordsUsed

确实,少了中间变量,更加清晰,还能再优化吗?

我们还可以进一步把整个处理流程封装到一个函数内:

function uniqueWords(str) {
    return unique( words( str ) );
}
uniqueWords(text)

这样就像是一个黑盒,无需管里面的流程,只用知道这个盒子输入是什么!输出是什么!输入输出清晰,功能清晰,非常“干净”!如图:

JS轻量级函数式编程实现XDM三

与此同时,它还能被搬来搬去,或再继续组装。

我们回到 uniqueWords() 函数的内部,它的数据流也是清晰的:

uniqueWords <-- unique <-- words <-- text

封装盒子

上面的封装 uniqueWords 盒子很 nice ,如果要不断的封装像 uniqueWords 的盒子,我们要一个一个的去写吗?

function uniqueWords(str) {
    return unique( words( str ) );
}
function uniqueWords_A(str) {
    return unique_A( words_A( str ) );
}
function uniqueWords_B(str) {
    return unique_B( words_B( str ) );
}
...

所以,一切为了偷懒,我们可以写一个功能更加强大的函数来实现自动封装盒子:

function compose2(fn2,fn1) {
    return function composed(origValue){
        return fn2( fn1( origValue ) );
    };
}
// ES6 箭头函数形式写法
var compose2 =
    (fn2,fn1) =>
        origValue =>
            fn2( fn1( origValue ) );

JS轻量级函数式编程实现XDM三

接着,调用就变成了这样:

var uniqueWords = compose2( unique, words );
var uniqueWords_A = compose2( unique_A, words_A );
var uniqueWords_B = compose2( unique_B, words_B );

太清晰了!

任意组合

上面,我们组合了两个函数,实际上我们也可以组合 N 个函数;

finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- origValue

JS轻量级函数式编程实现XDM三

比如用一个 compose 函数来实现(敲重点):

function compose(...fns) {
    return function composed(result){
        // 拷贝一份保存函数的数组
        var list = fns.slice();
        while (list.length > 0) {
            // 将最后一个函数从列表尾部拿出
            // 并执行它
            result = list.pop()( result );
        }
        return result;
    };
}
// ES6 箭头函数形式写法
var compose =
    (...fns) =>
        result => {
            var list = fns.slice();
            while (list.length > 0) {
                // 将最后一个函数从列表尾部拿出
                // 并执行它
                result = list.pop()( result );
            }
            return result;
        };

基于前面 uniqueWords(..) 的例子,我们进一步再增加一个函数来处理(过滤掉长度小于等于4的字符串):

function skipShortWords(list) {
    var filteredList = [];
    for (let i = 0; i < list.length; i++) {
        if (list[i].length > 4) {
            filteredList.push( list[i] );
        }
    }
    return filteredList;
}
var text = "To compose two functions together";
var biggerWords = compose( skipShortWords, unique, words );
var wordsUsed = biggerWords( text );
wordsUsed;
// ["compose", "functions", "together"]

这样 compose 函数就有三个入参且都是函数了。我们还可以利用偏函数的特性实现更多:

function skipLongWords(list) { /* .. */ }
var filterWords = partialRight( compose, unique, words ); // 固定 unique 函数 和 words 函数
var biggerWords = filterWords( skipShortWords );
var shorterWords = filterWords( skipLongWords );
biggerWords( text );
shorterWords( text );

filterWords 函数是一个更具有特定功能的变体(根据第一个函数的功能来过滤字符串)。

compose 变体

compose(..)函数非常重要,但我们可能不会在生产中使用自己写的 compose(..),而更倾向于使用某个库所提供的方案。了解其底层工作的原理,对我们强化理解函数式编程也非常有用。

我们理解下 compose(..) 的另一种变体 —— 递归的方式实现:

function compose(...fns) {
    // 拿出最后两个参数
    var [ fn1, fn2, ...rest ] = fns.reverse();
    var composedFn = function composed(...args){
        return fn2( fn1( ...args ) );
    };
    if (rest.length == 0) return composedFn;
    return compose( ...rest.reverse(), composedFn );
}
// ES6 箭头函数形式写法
var compose =
    (...fns) => {
        // 拿出最后两个参数
        var [ fn1, fn2, ...rest ] = fns.reverse();
        var composedFn =
            (...args) =>
                fn2( fn1( ...args ) );
        if (rest.length == 0) return composedFn;
        return compose( ...rest.reverse(), composedFn );
    };

通过递归进行重复的动作比在循环中跟踪运行结果更易懂,这可能需要更多时间去体会;

基于之前的例子,如果我们想让参数反转:

var biggerWords = compose( skipShortWords, unique, words );
// 变成
var biggerWords = pipe( words, unique, skipShortWords );

只需要更改 compose(..) 内部实现这一句就行:

...
        while (list.length > 0) {
            // 从列表中取第一个函数并执行
            result = list.shift()( result );
        }
...

虽然只是颠倒参数顺序,这二者没有本质上的区别。

抽象能力

你是否会疑问:什么情况下可以封装成上述的“盒子”呢?

这就很考验 —— 抽象的能力了!

实际上,有两个或多个任务存在公共部分,我们就可以进行封装了。

比如:

function saveComment(txt) {
    if (txt != "") {
        comments[comments.length] = txt;
    }
}
function trackEvent(evt) {
    if (evt.name !== undefined) {
        events[evt.name] = evt;
    }
}

就可以抽象封装为:

function storeData(store,location,value) {
    store[location] = value;
}
function saveComment(txt) {
    if (txt != "") {
        storeData( comments, comments.length, txt );
    }
}
function trackEvent(evt) {
    if (evt.name !== undefined) {
        storeData( events, evt.name, evt );
    }
}

在做这类抽象时,有一个原则是,通常被称作 DRY(don't repeat yourself),即便我们要花时间做这些非必要的工作。

抽象能让你的代码走得更远! 比如上例,还能进一步升级:

function conditionallyStoreData(store,location,value,checkFn) {
    if (checkFn( value, store, location )) {
        store[location] = value;
    }
}
function notEmpty(val) { return val != ""; }
function isUndefined(val) { return val === undefined; }
function isPropUndefined(val,obj,prop) {
    return isUndefined( obj[prop] );
}
function saveComment(txt) {
    conditionallyStoreData( comments, comments.length, txt, notEmpty );
}
function trackEvent(evt) {
    conditionallyStoreData( events, evt.name, evt, isPropUndefined );
}

这样 if 语句也被抽象封装了。

抽象是一个过程,程序员将一个名字与潜在的复杂程序片段关联起来,这样该名字就能够被认为代表函数的目的,而不是代表函数如何实现的。通过隐藏无关的细节,抽象降低了概念复杂度,让程序员在任意时间都可以集中注意力在程序内容中的可维护子集上。—— 《程序设计语言》

我们在本系列初始提到:“一切为了创造更可读、更易理解的代码。”

从另一个角度,抽象就是将命令式代码变成声命式代码的过程。从“怎么做”转化成“是什么”。

命令式代码主要关心的是描述怎么做来准确完成一项任务。声明式代码则是描述输出应该是什么,并将具体实现交给其它部分。

比如 ES6 增加的结构语法:

function getData() {
    return [1,2,3,4,5];
}
// 命令式
var tmp = getData();
var a = tmp[0];
var b = tmp[3];
// 声明式
var [ a ,,, b ] = getData();

开发者需要对他们程序中每个部分使用恰当的抽象级别保持谨慎,不能太过,也不能不够。

阶段小结

函数组合是为了符合“声明式编程风格”,即关注“是什么”,而非具体“做什么”。

它能将一个函数调用的输出路由跳转到另一个函数的调用上,然后一直进行下去,它借助 compose(..) 或它的变体实现。。

我们期望组合中的函数是一元的(输入输出尽量是一个),这个也是前篇有提到的很重要的一个点。

组合 ———— 声明式数据流 ———— 是支撑函数式编程其他特性的最重要的工具之一!

以上就是JS轻量级函数式编程实现XDM三的详细内容,更多关于JS轻量级函数式编程XDM的资料请关注三水点靠木其它相关文章!


Tags in this post...

Javascript 相关文章推荐
[HTML/CSS/Javascript]WWTJS
Sep 25 Javascript
使用typeof判断function是否存在于上下文
Aug 14 Javascript
javascript使用正则表达式检测IP地址
Dec 03 Javascript
javascript实现Table排序的方法
May 15 Javascript
JavaScript表单验证开发
Nov 23 Javascript
JS求解三元一次方程组值的方法
Jan 03 Javascript
jQuery Mobile漏洞会有跨站脚本攻击风险
Feb 12 Javascript
vue中计算属性(computed)、methods和watched之间的区别
Jul 27 Javascript
详解Angular2学习笔记之Html属性绑定
Jan 03 Javascript
vue-cli3.0 环境变量与模式配置方法
Nov 08 Javascript
layui2.0使用table+laypage实现真分页
Jul 27 Javascript
vue仿淘宝滑动验证码功能(样式模仿)
Dec 10 Javascript
JS轻量级函数式编程实现XDM二
Jun 16 #Javascript
JS函数式编程实现XDM一
Jun 16 #Javascript
正则表达式基础与常用验证表达式
Jun 16 #Javascript
使用compose函数优化代码提高可读性及扩展性
html中两种获取标签内的值的方法
Jun 16 #jQuery
JavaScript前端面试扁平数据转tree与tree数据扁平化
Jun 14 #Javascript
vue如何在data中引入图片的正确路径
Jun 05 #Vue.js
You might like
php缩放图片(根据宽高的等比例缩放)实例介绍
2013/06/09 PHP
php curl抓取网页的介绍和推广及使用CURL抓取淘宝页面集成方法
2015/11/30 PHP
微信公众号实现扫码获取微信用户信息(网页授权)
2019/04/09 PHP
ie和firefox中img对象区别的困惑
2006/12/27 Javascript
用js 让图片在 div或dl里 居中,底部对齐
2008/01/21 Javascript
Prototype RegExp对象 学习
2009/07/19 Javascript
js限制文本框为整数和货币的函数代码
2010/10/13 Javascript
jquery 实现checkbox全选,反选,全不选等功能代码(奇数)
2012/10/24 Javascript
提取字符串中年月日的函数代码
2013/11/05 Javascript
jquery.post用法关于type设置问题补充
2014/01/03 Javascript
使用nodejs、Python写的一个简易HTTP静态文件服务器
2014/07/18 NodeJs
javascript 事件处理示例分享
2014/12/31 Javascript
Bootstrap每天必学之基础排版
2015/11/20 Javascript
日常收集整理的JavaScript常用函数方法
2015/12/10 Javascript
JavaScript html5 canvas绘制时钟效果(二)
2016/03/27 Javascript
jQuery中使用animate自定义动画的方法
2016/05/29 Javascript
request请求获取参数的实现方法(post和get两种方式)
2016/09/27 Javascript
JavaScript实现通过select标签跳转网页的方法
2016/09/29 Javascript
js实现不提示直接关闭网页窗口
2017/03/30 Javascript
node中koa中间件机制详解
2017/08/22 Javascript
JS写XSS cookie stealer来窃取密码的步骤详解
2017/11/20 Javascript
jQuery 实现左右两侧菜单添加、移除功能
2018/01/02 jQuery
vuex中的 mapState,mapGetters,mapActions,mapMutations 的使用
2018/04/13 Javascript
微信小程序iBeacon测距及稳定程序的实现解析
2019/07/31 Javascript
[01:00:26]Ti4主赛事胜者组第一天 EG vs NEWBEE 1
2014/07/19 DOTA
python实现支付宝当面付(扫码支付)功能
2018/05/30 Python
pyqt5实现绘制ui,列表窗口,滚动窗口显示图片的方法
2019/06/20 Python
Python代码一键转Jar包及Java调用Python新姿势
2020/03/10 Python
Pycharm打开已有项目配置python环境的方法
2020/07/03 Python
python使用opencv resize图像不进行插值的操作
2020/07/05 Python
Mio Skincare英国官网:身体紧致及孕期身体护理
2018/08/19 全球购物
捐款倡议书范文
2014/02/02 职场文书
乡镇纠风工作实施方案
2014/03/22 职场文书
代办出身证明书
2014/10/21 职场文书
优秀团员主要事迹范文
2015/11/05 职场文书
HTML+CSS实现导航条下拉菜单的示例代码
2021/08/02 HTML / CSS