前端学习函数式编程的方法和误区

本文详细阐述了函数式编程中的无副作用函数及其优势,包括缓存、并发执行和测试便利性。并通过实例展示了如何使用Map和偏函数、高阶函数进行函数组合,以及Monad和递归的概念。
摘要由CSDN通过智能技术生成
  1. 要有输入参数。如果没有输入参数,这个函数拿不到任意外部信息,也就不用运行了。

  2. 要有返回值。如果有输入没有返回值,又没有副作用,那么这个函数白调了。

  3. 对于确定的输入,有确定的输出

做到这一点,说简单也简单,只要保持功能足够简单就可以做到;说困难也困难,需要改变写惯了命令行代码的思路。

比如数学函数一般就是这样的好例子,比如我们写一个算平方的函数:

let sqr2 = function(x){

return x * x;

}

console.log(sqr2(200));

无副作用函数拥有三个巨大的好处:

  1. 可以进行缓存。我们就可以采用动态规划的方法保存中间值,用来代替实际函数的执行结果,大大提升效率。

  2. 可以进行高并发。因为不依赖于环境,可以调度到另一个线程、worker甚至其它机器上,反正也没有环境依赖。

  3. 容易测试,容易证明正确性。不容易产生偶现问题,也跟环境无关,非常利于测试。

即使是跟有副作用的代码一起工作,我们也可以在副作用代码中缓存无副作用函数的值,可以将无副作用函数并发执行。测试时也可以更重点关注有副作用的代码以更有效地利用资源。

用函数的组合来替代命令的组合

479eab51a54de389e7c16e6eea43521a.png

会写无副作用的函数之后,我们要学习的新问题就是如何将这些函数组合起来。

比如上面的sqr2函数有个问题,如果不是number类型,计算就会出错。按照命令式的思路,我们可能就直接去修改sqr2的代码,比如改成这样:

let sqr2 = function(x){

if (typeof x === ‘number’){

return x * x;

}else{

return 0;

}

}

但是,sqr2的代码已经测好了,我们能不能不改它,只在它外面进行判断?

是的,我们可以这样写:

let isNum = function(x){

if (typeof x === ‘number’){

return x;

}else{

return 0;

}

}

console.log(sqr2(isNum(“20”)));

或者是我们在设计sqr2的时候就先预留出来一个预处理函数的位置,将来要升级就换这个预处理函数,主体逻辑不变:

let sqr2_v3 = function(fn, x){

let y = fn(x);

return y * y;

}

console.log((sqr2_v3(isNum,1.1)));

嫌每次都写isNum烦,可以定义个新函数,把isNum给写死进去:

let sqr2_v4 = function(x){

return sqr2_v3(isNum,x);

}

console.log((sqr2_v4(2.2)));

用容器封装函数能力

e16879b288abc4c9b897b9591e359402.png

现在,我们想重用这个isNum的能力,不光是给sqr2用,我们想给其它数学函数也增加这个能力。

比如,如果给Math.sin计算undefined会得到一个NaN:

console.log(Math.sin(undefined));

这时候我们需要用面向对象的思维了,将isNum的能力封装到一个类中:

class MayBeNumber{

constructor(x){

this.x = x;

}

map(fn){

return new MayBeNumber(fn(isNum(this.x)));

}

getValue(){

return this.x;

}

}

这样,我们不管拿到一个什么对象,用其构造一个MayBeNumber对象出来,再调用这个对象的map方法去调用数学函数,就自带了isNum的能力。

我们先看调用sqr2的例子:

let num1 = new MayBeNumber(3.3).map(sqr2).getValue();

console.log(num1);

let notnum1 = new MayBeNumber(undefined).map(sqr2).getValue();

console.log(notnum1);

我们可以将sqr2换成Math.sin:

let notnum2 = new MayBeNumber(undefined).map(Math.sin).getValue();

console.log(notnum2);

可以发现,输出值从NaN变成了0.

封装到对象中的另一个好处是我们可以用"."多次调用了,比如我们想调两次算4次方,只要在.map(sqr2)之后再来一个.map(sqr2)

let num3 = new MayBeNumber(3.5).map(sqr2).map(sqr2).getValue();

console.log(num3);

使用对象封装之后的另一个好处是,函数嵌套调用跟命令式是相反的顺序,而用map则与命令式一致。

如果不理解的话我们来举个例子,比如我们想求sin(1)的平方,用函数调用应该先写后执行的sqr2,后写先执行的Math.sin:

console.log(sqr2(Math.sin(1)));

而调用map就跟命令式一样了:

let num4 = new MayBeNumber(1).map(Math.sin).map(sqr2).getValue();

console.log(num4);

用 of 来封装 new

f657292256440739da50d1cb86e032ba.png

封装到对象中,看起来还不错,但是函数式编程还搞出来new对象再map,为什么不能构造对象时也用个函数呢?

这好办,我们给它定义个of方法吧:

MayBeNumber.of = function(x){

return new MayBeNumber(x);

}

下面我们就可以用of来构造MayBeNumber对象啦:

let num5 = MayBeNumber.of(1).map(Math.cos).getValue();

console.log(num5);

let num6 = MayBeNumber.of(2).map(Math.tan).map(Math.exp).getValue();

console.log(num6);

有了of之后,我们也可以给map函数升升级。

之前的isNum有个问题,如果是非数字的话,其实没必要赋给个0再去调用函数,直接返回个0就好了。

之前我们一直没写过箭头函数,顺手写一写:

isNum2 = x => typeof x === ‘number’;

map用isNum2和of改写下:

map(fn){

if (isNum2(this.x)){

return MayBeNumber.of(fn(this.x));

}else{

return MayBeNumber.of(0);

}

}

我们再来看下另一种情况,我们处理返回值的时候,如果有Error,就不处理Ok的返回值,可以这么写:

class Result{

constructor(Ok, Err){

this.Ok = Ok;

this.Err = Err;

}

isOk(){

return this.Err === null || this.Err === undefined;

}

map(fn){

return this.isOk() ? Result.of(fn(this.Ok),this.Err) : Result.of(this.Ok, fn(this.Err));

}

}

Result.of = function(Ok, Err){

return new Result(Ok, Err);

}

console.log(Result.of(1.2,undefined).map(sqr2));

输出结果为:

Result { Ok: 1.44, Err: undefined }

我们来总结下前面这种容器的设计模式:

  1. 有一个用于存储值的容器

  2. 这个容器提供一个map函数,作用是map函数使其调用的函数可以跟容器中的值进行计算,最终返回的还是容器的对象

我们可以把这个设计模式叫做Functor函子。

如果这个容器还提供一个of函数将值转换成容器,那么它叫做Pointed Functor.

比如我们看下js中的Array类型:

let aa1 = Array.of(1);

console.log(aa1);

console.log(aa1.map(Math.sin));

它支持of函数,它还支持map函数调用Math.sin对Array中的值进行计算,map的结果仍然是一个Array。

那么我们可以说,Array是一个Pointed Functor。

简化对象层级

b1870f843bd27ed7c2747e4057c25aa6.png

有了上面的Result结构了之后,我们的函数也跟着一起升级。如果是数值的话,Ok是数值,Err是undefined。如果非数值的话,Ok是undefined,Err是0:

let sqr2_Result = function(x){

if (isNum2(x)){

return Result.of(x*x, undefined);

}else{

return Result.of(undefined,0);

}

}

我们调用这个新的sqr2_Result函数:

console.log(Result.of(4.3,undefined).map(sqr2_Result));

返回的是一个嵌套的结果:

Result { Ok: Result { Ok: 18.49, Err: undefined }, Err: undefined }

我们需要给Result对象新加一个join函数,用来获取子Result的值给父Result:

join(){

if (this.isOk()) {

return this.Ok;

}else{

return this.Err;

}

}

我们调用的时候最后加上调用这个join:

console.log(Result.of(4.5,undefined).map(sqr2_Result).join());

嵌套的结果变成了一层的:

Result { Ok: 20.25, Err: undefined }

每次调用map(fn).join()两个写起来麻烦,我们定义一个flatMap函数一次性处理掉:

flatMap(fn){

return this.map(fn).join();

}

调用方法如下:

console.log(Result.of(4.7,undefined).flatMap(sqr2_Result));

结果如下:

Result { Ok: 22.090000000000003, Err: undefined }

我们最后完整回顾下这个Result:

class Result{

constructor(Ok, Err){

this.Ok = Ok;

this.Err = Err;

}

isOk(){

return this.Err === null || this.Err === undefined;

}

map(fn){

return this.isOk() ? Result.of(fn(this.Ok),this.Err) : Result.of(this.Ok, fn(this.Err));

}

join(){

if (this.isOk()) {

return this.Ok;

}else{

return this.Err;

}

}

flatMap(fn){

return this.map(fn).join();

}

}

Result.of = function(Ok, Err){

return new Result(Ok, Err);

}

不严格地讲,像Result这种实现了flatMap功能的Pointed Functor,就是传说中的Monad。

偏函数和高阶函数

0d6a9a7c015a76271797c847532e3ccd.png

在前面各种函数式编程模式中对函数的用法熟悉了之后,回来我们总结下函数式编程与命令行编程体感上的最大区别:

  1. 函数是一等公式,我们应该熟悉变量中保存函数再对其进行调用

  2. 函数可以出现在返回值里,最重要的用法就是把输入是n(n>2)个参数的函数转换成n个1个参数的串联调用,这就是传说中的柯里化。这种减少了参数的新函数,我们称之为偏函数

  3. 函数可以用做函数的参数,这样的函数称为高阶函数

偏函数可以当作是更灵活的参数默认值。

比如我们有个结构叫spm,由spm_a和spm_b组成。但是一个模块中spm_a是固定的,大部分时候只需要指定spm_b就可以了,我们就可以写一个偏函数:

const getSpm = function(spm_a, spm_b){

return [spm_a, spm_b];

}

const getSpmb = function(spm_b){

return getSpm(1000, spm_b);

}

console.log(getSpmb(1007));

高阶函数我们在前面的map和flatMap里面已经用得很熟了。但是,其实高阶函数值得学习的设计模式还不少。

比如给大家出一个思考题,如何用函数式方法实现一个只执行一次有效的函数?

2bfb3571b89ced8283d163e51d886d96.png

不要用全局变量啊,那不是函数式思维,我们要用闭包。

once是一个高阶函数,返回值是一个函数,如果done是false,则将done设为true,然后执行fn。done是在返回函数的同一层,所以会被闭包记忆获取到:

const once = (fn) => {

let done = false;

return function() {

return done ? undefined : ((done=true), fn.apply(this,arguments));

}

}

let init_data = once(

() => {

console.log(“Initialize data”);

}

);

init_data();

init_data();

我们可以看到,第二次调用init_data()没有发生任何事情。

递归与记忆

前面介绍了这么多,但是函数编程其实还蛮复杂的,比如说涉及到递归。

递归中最简单的就是阶乘了吧:

let factorial = (n) => {

if (n===0){

return 1;

}

return n*factorial(n-1);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。

《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》PDF完整版点击这里免费领取

前端面试题宝典

前端校招面试题详解

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-uV4jWXsi-1713719531045)]

[外链图片转存中…(img-wRy0Wu4z-1713719531045)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-qSBKSwMx-1713719531046)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

[外链图片转存中…(img-uQmtNHWB-1713719531046)]

最后

整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。

《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》PDF完整版点击这里免费领取

[外链图片转存中…(img-pa0iZx5o-1713719531046)]

[外链图片转存中…(img-FLf1CqFV-1713719531047)]

[外链图片转存中…(img-lDFrGCi7-1713719531047)]

  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值