前端必学-函数式编程(六)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端必学-函数式编程(六)相关的知识,希望对你有一定的参考价值。

参考技术A

我们前篇谈了很多关于【闭包】的理解了,所以你应该会知道,我们现在将要谈的就是 ——【异步】。

我们为什么觉得“异步问题”复杂呢?

其中很重要的一个原因是 —— 时间!时间将我们对数据的操作、管理,变复杂了好几个量级!

(需要特别提出并明确的是: 异步和同步之间是可以相互转化的! 我们使用异步或者同步取决于 —— 如何使代码更加可读!)

函数式编程给出了实现“代码更可读”的落地原则(已多次回顾):

所以我们可以期待,异步在函数式编程中的表现!

上代码:

onCustomer(..) 和 onOrders(..) 是两个【回调函数】释义,两者执行的先后顺序并不能确定,所以它是一个基于时间的复杂状态。

释义:回调函数其实就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。

通常来说,我们最先想到的是:把 lookupOrders(..) 写到 onCustomer(..) 里面,那我们就可以确认 onOrders(..) 会在 onCustomer(..) 之后运行。

这样写,对吗?

不对!因为 onCustomer(..) 、 onOrders(..) 这两个回调函数的关系更像是一种竞争关系(都是赋值 customer.orders ), 它们应该并行执行 而不是串行执行

即:我不管你们谁先执行,谁先执行完,谁就赋值给 customer.orders !

那我们的思路应该是:

不过,这样让代码又变得更加难阅读!!函数内部赋值依赖于外部变量、甚至受外部回调函数的影响。

那究竟怎么办呢?

最终,我们借用 JS promise 减少这个时间状态,将异步转成同步:

两个 .then(..) 运行之前, lookupCustomer(..) 和 lookupOrders(..) 已被同步调用,满足并行执行,谁先结束,谁赋值给 customer.orders ,所以我们不需要知道谁先谁后!

在这样的实现下,不再需要时间先后的概念!减少了时间状态!!代码的可读性更高了!!

这是一个 积极的数组 ,因为它们同步(即时)地操作着离散的即时值或值的列表/结构上的值。

什么意思?

a 映射到 b,再去修改 a ,b 不会收到影响。

而这,是一个 惰性的数组 , mapLazy(..) 本质上 “监听” 了数组 a,只要一个新的值添加到数组的末端(push(..)),它都会运行映射函数 v => v * 2 并把改变后的值添加到数组 b 里。

什么意思?

a 映射到 b,再去修改 a ,b 也会修改。

原来,后者存在 异步 的概念。

让我们来想象这样一个数组,它不只是简单地获得值,它还是一个懒惰地接受和响应(也就是“反应”)值的数组,比如:

设置“懒惰的数组” a 的过程是异步的!

b ,是 map 映射后的数组,但更重要的是,b 是 反应性 的,我们对 b 加了一个类似监听器的东西。

这里直接给出解答:

这里再多小结一句:时间让异步更加复杂,函数式编程在异步下的运用就是减少或直接干掉时间状态。

想象下 a 还可以被绑定上一些其他的事件上,比如说用户的鼠标点击事件和键盘按键事件,服务端来的 websocket 消息等。

上述的 LazyArray 又可叫做 observable !(当然,它不止用在 map 方法中)

现在已经有各种各样的 Observables 的库类,最出名的是 RxJS Most

以 RxJS 为例:

不仅如此,RxJS 还定义了超过 100 个可以在有新值添加时才触发的方法。就像数组一样。每个 Observable 的方法都会返回一个新的 Observable,意味着他们是链式的。如果一个方法被调用,则它的返回值应该由输入的 Observable 去返回,然后触发到输出的 Observable里,否则抛弃。

比如:

本篇介绍了【异步】在函数式编程中的表现。

原则是:对于那些异步中有时态的操作,基础的函数式编程原理就是将它们变为无时态的应用。即 减少时间状态

就像 promise 创建了一个单一的未来值,我们可以创建一个积极的列表的值来代替像惰性的observable(事件)流的值。

我们介绍了 RxJS 库,后续我们还会介绍更多优美的 JS 函数式编程库!

(俗话说的好,三方库选的好,下班都很早!!)

现在本瓜有点明白那句话了:看一门语言是不是函数式编程,取决于它的核心库是不是函数式编程。

也许我们还不熟悉像 RxJS 这类库,但我们慢慢就会越来越重视它们,越来越使用它们,越来越领会到它们!!

异步,以上。

前端必学——函数式编程

前文梳理

第一篇

  1. 为什么要进行函数式编程?—— 一切只是为了代码更加可读!!
  2. 开发人员喜欢【显式】输入输出而不是【隐式】输入输出,要明白何为显式,何为隐式!!
  3. 一个函数如果可以接受或返回一个甚至多个函数,它被叫做高阶函数。闭包是最强大的高阶函数!!

第二篇

讲了重要的两个概念:偏函数柯里化

  1. 函数组装是函数式编程最重要的实现方式!而熟练运用偏函数、柯里化,以及它们的变体,是函数组装的基础。
  2. 偏函数表现形式:partial(sum,1,2)(3)
  3. 柯里化表现形式:sum(1)(2)(3)

第三篇

“函数组装”这一重点:

  1. 再次重申,函数组装是函数式编程最重要的实现方式!!
  2. 函数组装符合 “声明式编程风格”,即声明的时候你就知道了它“是什么”!而不用知道它具体“干了什么”(命令式函数风格)!
  3. 比如:当你看到组装后的函数调用是这样,compose( skipShortWords, unique, words )( text ),就知道了它是先将 text 变成 words,然后 unique 去重,然后过滤较短长度的 words。非常清晰!
  4. compose(..)函数和partial(..)函数结合,可以实现丰富多彩的组装形式!
  5. 封装抽象成函数是一门技术活!不能不够,也不宜太过!

第四篇

再细扣了下 “副作用”

  1. 开发人员喜欢显式输入输出而不是隐式输入输出,学函数式编程,这句话要深入骨髓的记忆!
  2. 解决副作用的方法有:定义常量、明确 I/O、明确依赖、运用幂等,记得对幂等留个心眼!
  3. 我们喜欢没有副作用的函数,即纯函数!!
  4. 假如一棵树在森林里倒下而没有人在附近听见,它有没有发出声音?——对于这个问题的理解就是:假如你封装了一个高级函数,在内部即使有副作用的情况下,外界会知道这个信息吗,它还算是纯函数吗?

以上便是我们的简要回顾!

我们可能还需要更多时间去实践和体会:

  1. 偏函数 partial(..)和函数组装compose(..)的变体及应用;
  2. 抽象的能力;
  3. 封装高级的纯函数;

第五篇,咱们将基于实践,分享最最常见的现象 —— 数组操作,看看它是如体现函数式编程精神!

数组三剑客

这三剑客是:map(..)filter(..) 和 reduce(..)

map

我们都会用 ES6 map(..) , 它“是什么”,我们非常清楚!

前端必学——函数式编程(五)_偏函数

轻松写一个 map(..) 的使用:

[1,2,3].map(item => item + 1)

但是,map(..) “干了什么”,即它的内部是怎样的,你知道吗?

我们可以用原生实现一个函数 map(..) :

function map(mapperFn,arr) 
var newList = [];

for (let id = 0; id < arr.length; id++)
newList.push(
mapperFn( arr[id], id, arr )
);


return newList;


map(item=>item+1,[1,2,3])

我们把一个 mapperFn(..) 封装进模拟的 map(..) 函数内,其内部也是 for 循环遍历。

我们还可以用 map(..) 做更多:

比如先将函数放在列表中,然后组合列表中的每一个函数,最后执行它们,像这样:

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(..)的本质是过滤值。如图示意:


前端必学——函数式编程(五)_数组_02

[1,2,3].filter(item => item>2)

手写一个 filter(..) 函数:

function filter(predicateFn,arr) 
var newList = [];

for (let id = 0; id < arr.length; id++)
if (predicateFn( arr[id], id, arr ))
newList.push( arr[id] );



return newList;


filter(item=>item>2,[1,2,3])

同样也是将一个函数作为入参,处理同样传入的 arr,遍历过滤得到目标数组;

reduce

map(..) 和 filter(..) 都会产生新的数组,而第三种操作(reduce(..))则是典型地将列表中的值合并(或减少)到单个值(非列表)。


前端必学——函数式编程(五)_函数式编程_03

[5,10,15].reduce( (product,v) => product * v, 3 );

过程:

  1. 3 * 5 = 15
  2. 15 * 10 = 150
  3. 150 * 15 = 2250

手动实现 reduce 函数相较前两个,要稍微复杂些:

function reduce(reducerFn,initialValue,arr) 
var acc, startId;

if (arguments.length == 3)
acc = initialValue;
startId = 0;

else if (arr.length > 0)
acc = arr[0];
startId = 1;

else
throw new Error( "Must provide at least one value." );


for (let id = startId; id < arr.length; id++)
acc = reducerFn( acc, arr[id], id, arr );


return acc;

不像 map(..) 和 filter(..) ,对传入数组的次序没有要求。reduce(..) 明确要采用从左到右的处理方式。

高级操作

基于 map(..)filter(..) 和 reduce(..),我们再看些更复杂的操作;

去重

实现:

var unique =
arr =>
arr.filter(
(v,id) =>
arr.indexOf( v ) == id
);

unique( [1,4,7,1,3,1,7,9,2,6,4,0,5,3] );

原理是,当从左往右筛选元素时,列表项的 id 位置和 indexOf(..) 找到的位置相等时,表明该列表项第一次出现,在这种情况下,将列表项加入到新数组中。

当然,去重方式有很多,但是,这种方式的优点是,它们使用了内建的列表操作,它们能更方便的和其他列表操作链式/组合调用。

这里也写一下reduce(..) 实现:

var unique =
arr =>
arr.reduce(
(list,v) =>
list.indexOf( v ) == -1 ?
( list.push( v ), list ) : list
, [] );

降维

二位数组转一维数组

[ [1, 2, 3], 4, 5, [6, [7, 8]] ] => [ 1, 2, 3, 4, 5, 6, 7, 8 ]

实现:

var flatten =
arr =>
arr.reduce(
(list,v) =>
list.concat( Array.isArray( v ) ? flatten( v ) : v )
, [] );

你还可以加一个参数 depth 来指定降维的层数:

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]]]

看到这里,如果觉得复杂,你可以只把它作为一个库来调用即可。实际上,我们后续还会专门来介绍各类函数式编程函数库

融合

仔细体会下,以下给出的三段代码,哪段你觉得你更容易看懂?哪一段更符合函数式编程?

// 实现 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 中无法抉择?

再看一例:

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 isnt responsible for this disaster!"
.split( /\\s/ );

words;
// ["Mr.","Jones","isnt","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 这种融合的技术,是常见的性能优化方式。

阶段小结

以上,我们看到了:

三个强大通用的列表操作:

  1. map(..): 转换列表项的值到新列表;
  2. filter(..): 选择或过滤掉列表项的值到新数组;
  3. reduce(..): 合并列表中的值,并且产生一个其他的值(也可能是非列表的值);

这是我们平常用的最多的数组遍历方式,但这次我们借助函数式编程思想把它们升级了!

这些高级操作:unique(..)、flatten(..)、map 融合的思想等(其实还有很多其它高级操作),值得我们去研究、感受体会,最后运用到实践中去!!

我是掘金安东尼: 一名人气前端技术博主(文章 100w+ 阅读量)

终身写作者(INFP 写作人格)

坚持与热爱(简书打卡 1000 日)

我能陪你一起度过漫长技术岁月吗(以梦为马)

觉得不错,给个点赞和关注吧(这是我最大的动力 )b( ̄▽ ̄)d

以上是关于前端必学-函数式编程(六)的主要内容,如果未能解决你的问题,请参考以下文章

前端必学——函数式编程

前端必学——函数式编程

前端必学——函数式编程

前端学习之函数式编程—函数式编程概念+头等函数

大前端进击之路:函数式编程

函数式夜点心:Monad