本篇内容介绍了“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;
这样的过程就好比生产线上加工商品,流水线加工。
想象一下,如果你是工厂老板,还会怎样优化流程、节约成本?
这里作者给了一种解决方式:去掉传送带!
即减少中间变量,我们可以这样调用:
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(..),而更倾向于使用某个库所提供的方案。了解其底层工作的原理,对我们强化理解函数式编程也非常有用。
我们理解下 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();
“JS组合函数实例分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!