JS函数式编程中组合函数的内容如何理解
Admin 2022-09-08 群英技术资讯 271 次浏览
函数编程就像拼乐高!
乐高有各式各样的零部件,我们将它们组装拼接,拼成一个更大的组件或模型。
函数编程也有各种功能的函数,我们将它们组装拼接,用于实现某个特定的功能。
下面来看一个例子,比如我们要使用这两个函数来分析文本字符串:
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;
这样的过程就好比生产线上加工商品,流水线加工。
想象一下,如果你是工厂老板,还会怎样优化流程、节约成本?
这里作者给了一种解决方式:去掉传送带!
即减少中间变量,我们可以这样调用:
var wordsUsed = unique( words( text ) );
wordsUsed
确实,少了中间变量,更加清晰,还能再优化吗?
我们还可以进一步把整个处理流程封装到一个函数内:
function uniqueWords(str) {
return unique( words( str ) );
}
uniqueWords(text)
这样就像是一个黑盒,无需管里面的流程,只用知道这个盒子输入是什么!输出是什么!输入输出清晰,功能清晰,非常“干净”!如图:
与此同时,它还能被搬来搬去,或再继续组装。
我们回到 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 ) );
接着,调用就变成了这样:
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
比如用一个 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(..) 的另一种变体 —— 递归的方式实现:
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函数式编程中组合函数的内容如何理解”的学习就结束了,希望能够解决大家的疑惑,另外大家动手实践也很重要,对大家加深理解和学习很有帮助。如果想要学习更多的相关知识,欢迎关注群英网络资讯站,小编每天都会给大家分享实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
猜你喜欢
对于发布订阅模式想必大家并不陌生,它在异步交互中具有很大的作用,能够使我们的代码结构更加清晰易读,便于维护。在node中我们可以使用 内置模块event 来实现发布订阅模式,这篇文章我们将深入去学习event并演示它在我们实际开发中的作用,让我们开始吧!
这篇文章主要介绍vue.js Router嵌套路由,平时访问首页,里面有新闻类的/home/news,还有信息类的/home/message这时候就需要使用到嵌套路由,下面我们就来具体学习嵌套路由的原理,需要的朋友可以参考一下
传统的写法,这种需要自己去控制内部的状态。为了可以实现对象的遍历,我们需要在对象上实现上面说的迭代器。另外一种是利用生成器函数返回的Generator的迭代器来实现。
本文主要实现给项目的整个背景加上自定义水印,可以改变水印的文案和字体颜色等,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
前几天做到一个关于图片上传功能,下面这篇文章主要给大家介绍了关于vue使用element实现上传图片和修改图片功能的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所有
增值电信经营许可证 : B1.B2-20140078 粤ICP备09006778号 域名注册商资质 粤 D3.1-20240008