theme: geek-black
本篇是《JS如何函数式编程》系列第五篇!共七篇,似乎已经能望见胜利的彼岸了!!!
纪伯伦曾说过:我们都已经走的太久了,以至于忘了为什么出发。
We already walked too far, down to we had forgotten why embarked.
所以,第五篇开始前,咱们不如先来一个对前面每篇的梳理:
前文梳理
第一篇
《XDM,JS如何函数式编程?看这就够了!(一)》,作为“纲要篇”,重点解释了:
- 本系列是基于《medium 五万赞好文-《我永远不懂 JS 闭包》》和《“类”设计模式和“原型”设计模式——“复制”和“委托”的差异》两篇的延伸探索,推荐阅读。
- 为什么要进行函数式编程?—— 一切只是为了代码更加可读!!
- 开发人员喜欢【显式】输入输出而不是【隐式】输入输出,要明白何为显式,何为隐式!!
- 一个函数如果可以接受或返回一个甚至多个函数,它被叫做高阶函数。闭包是最强大的高阶函数!!
第二篇
《XDM,JS如何函数式编程?看这就够了!(二)》,讲了重要的两个概念:偏函数、柯里化
- 函数组装是函数式编程最重要的实现方式!而熟练运用偏函数、柯里化,以及它们的变体,是函数组装的基础。
- 偏函数表现形式:
partial(sum,1,2)(3)
- 柯里化表现形式:
sum(1)(2)(3)
第三篇
《XDM,JS如何函数式编程?看这就够了!(三)》,来到了“函数组装”这一重点:
- 再次重申,函数组装是函数式编程最重要的实现方式!!
- 函数组装符合 “声明式编程风格”,即声明的时候你就知道了它“是什么”!而不用知道它具体“干了什么”(命令式函数风格)!
- 比如:当你看到组装后的函数调用是这样,
compose( skipShortWords, unique, words )( text )
,就知道了它是先将 text 变成 words,然后 unique 去重,然后过滤较短长度的 words。非常清晰! compose(..)
函数和partial(..)
函数结合,可以实现丰富多彩的组装形式!- 封装抽象成函数是一门技术活!不能不够,也不宜太过!
第四篇
《XDM,JS如何函数式编程?看这就够了!(四)》,我们再细扣了下 “副作用”:
- 开发人员喜欢显式输入输出而不是隐式输入输出,学函数式编程,这句话要深入骨髓的记忆!
- 解决副作用的方法有:定义常量、明确 I/O、明确依赖、运用幂等,记得对幂等留个心眼!
- 我们喜欢没有副作用的函数,即纯函数!!
- 假如一棵树在森林里倒下而没有人在附近听见,它有没有发出声音?——对于这个问题的理解就是:假如你封装了一个高级函数,在内部即使有副作用的情况下,外界会知道这个信息吗,它还算是纯函数吗?
以上便是我们的简要回顾!
我们可能还需要更多时间去实践和体会:
- 偏函数
partial(..)
和函数组装compose(..)
的变体及应用; - 抽象的能力;
- 封装高级的纯函数;
OK!温故知新,yyds!
第五篇,咱们将基于实践,分享最最常见的现象 —— 数组操作,看看它是如体现函数式编程精神!
数组三剑客
这三剑客是:map(..)
、filter(..)
和 reduce(..)
。
map
我们都会用 ES6 map(..)
, 它“是什么”,我们非常清楚!
轻松写一个 map(..)
的使用:
[1,2,3].map(item => item 1)
但是,map(..)
“干了什么”,即它的内部是怎样的,你知道吗?
我们可以用原生实现一个函数 map(..)
:
function map(mapperFn,arr) {
var newList = [];
for (let idx = 0; idx < arr.length; idx ) {
newList.push(
mapperFn( arr[idx], idx, arr )
);
}
return newList;
}
map(item=>item 1,[1,2,3])
我们把一个 mapperFn(..)
封装进模拟的 map(..)
函数内,其内部也是 for 循环遍历。
我们还可以用 map(..)
做更多:
比如先将函数放在列表中,然后组合列表中的每一个函数,最后执行它们,像这样:
代码语言:javascript复制var increment = v => v;
var decrement = v => --v;
var square = v => v * v;
var double = v => v * 2;
[increment,decrement,square]
.map( fn => compose( fn, double ) )
.map( fn => fn( 3 ) );
// [7,5,36]
细细品一品~
filter
如果说map(..)
的本质是映射值,filter(..)
的本质是过滤值。如图示意:
代码语言:javascript复制[1,2,3].filter(item => item>2)
手写一个 filter(..)
函数:
function filter(predicateFn,arr) {
var newList = [];
for (let idx = 0; idx < arr.length; idx ) {
if (predicateFn( arr[idx], idx, arr )) {
newList.push( arr[idx] );
}
}
return newList;
}
filter(item=>item>2,[1,2,3])
同样也是将一个函数作为入参,处理同样传入的 arr,遍历过滤得到目标数组;
reduce
map(..)
和 filter(..)
都会产生新的数组,而第三种操作(reduce(..))则是典型地将列表中的值合并(或减少)到单个值(非列表)。
代码语言:javascript复制[5,10,15].reduce( (product,v) => product * v, 3 );
过程:
- 3 * 5 = 15
- 15 * 10 = 150
- 150 * 15 = 2250
手动实现 reduce 函数相较前两个,要稍微复杂些:
代码语言:javascript复制function reduce(reducerFn,initialValue,arr) {
var acc, startIdx;
if (arguments.length == 3) {
acc = initialValue;
startIdx = 0;
}
else if (arr.length > 0) {
acc = arr[0];
startIdx = 1;
}
else {
throw new Error( "Must provide at least one value." );
}
for (let idx = startIdx; idx < arr.length; idx ) {
acc = reducerFn( acc, arr[idx], idx, arr );
}
return acc;
}
不像 map(..)
和 filter(..)
,对传入数组的次序没有要求。reduce(..)
明确要采用从左到右的处理方式。
高级操作
基于 map(..)
、filter(..)
和 reduce(..)
,我们再看些更复杂的操作;
去重
实现:
代码语言:javascript复制var unique =
arr =>
arr.filter(
(v,idx) =>
arr.indexOf( v ) == idx
);
unique( [1,4,7,1,3,1,7,9,2,6,4,0,5,3] );
原理是,当从左往右筛选元素时,列表项的 idx 位置和 indexOf(..) 找到的位置相等时,表明该列表项第一次出现,在这种情况下,将列表项加入到新数组中。
当然,去重方式有很多,但是,这种方式的优点是,它们使用了内建的列表操作,它们能更方便的和其他列表操作链式/组合调用。
这里也写一下reduce(..)
实现:
var unique =
arr =>
arr.reduce(
(list,v) =>
list.indexOf( v ) == -1 ?
( list.push( v ), list ) : list
, [] );
降维
二位数组转一维数组
代码语言:javascript复制[ [1, 2, 3], 4, 5, [6, [7, 8]] ] => [ 1, 2, 3, 4, 5, 6, 7, 8 ]
实现:
代码语言:javascript复制var flatten =
arr =>
arr.reduce(
(list,v) =>
list.concat( Array.isArray( v ) ? flatten( v ) : v )
, [] );
你还可以加一个参数 depth 来指定降维的层数:
代码语言:javascript复制var flatten =
(arr,depth = Infinity) =>
arr.reduce(
(list,v) =>
list.concat(
depth > 0 ?
(depth > 1 && Array.isArray( v ) ?
flatten( v, depth - 1 ) :
v
) :
[v]
)
, [] );
flatten( [[0,1],2,3,[4,[5,6,7],[8,[9,[10,[11,12],13]]]]], 2 );
// [0,1,2,3,4,5,6,7,8,[9,[10,[11,12],13]]]
看到这里,如果觉得复杂,你可以只把它作为一个库来调用即可。实际上,我们后续还会专门来介绍各类函数式编程函数库!
融合
仔细体会下,以下给出的三段代码,哪段你觉得你更容易看懂?哪一段更符合函数式编程?
代码语言:javascript复制// 实现 1
[1,2,3,4,5]
.filter( isOdd )
.map( double )
.reduce( sum, 0 ); // 18
// 实现 2
reduce(
map(
filter( [1,2,3,4,5], isOdd ),
double
),
sum,
0
); // 18
// 实现 3
compose(
partialRight( reduce, sum, 0 ),
partialRight( map, double ),
partialRight( filter, isOdd )
)
( [1,2,3,4,5] ); // 18
在片段 1 和 片段 3 中无法抉择?
再看一例:
代码语言:javascript复制var removeInvalidChars = str => str.replace( /[^w]*/g, "" );
var upper = str => str.toUpperCase();
var elide = str =>
str.length > 10 ?
str.substr( 0, 7 ) "..." :
str;
var words = "Mr. Jones isn't responsible for this disaster!"
.split( /s/ );
words;
// ["Mr.","Jones","isn't","responsible","for","this","disaster!"]
// 片段 1
words
.map( removeInvalidChars )
.map( upper )
.map( elide );
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]
// 片段 3
words
.map(
compose( elide, upper, removeInvalidChars )
);
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]
重点就是:
我们可以将那三个独立的相邻的 map(..) 调用步骤看成一个转换组合。因为它们都是一元函数,并且每一个返回值都是下一个点输入值。我们可以采用 compose(..) 执行映射功能,并将这个组合函数传入到单个 map(..) 中调用:
所以:片段 3 这种融合的技术,是常见的性能优化方式。
阶段小结
以上,我们看到了:
三个强大通用的列表操作:
- map(..): 转换列表项的值到新列表;
- filter(..): 选择或过滤掉列表项的值到新数组;
- reduce(..): 合并列表中的值,并且产生一个其他的值(也可能是非列表的值);
这是我们平常用的最多的数组遍历方式,但这次我们借助函数式编程思想把它们升级了!
这些高级操作:unique(..)、flatten(..)、map 融合的思想等(其实还有很多其它高级操作),值得我们去研究、感受体会,最后运用到实践中去!!
OK~ 本次就到这里,期待下次再会 ~
我是掘金安东尼,公众号【掘金安东尼】,输入暴露输入,技术洞见生活!