通用的函数式编程语言,是Haskell,被函数式原教旨主义者认为是纯函数式语言。函数式编程的思想也不断影响着传统编程语言,比如Java 8开始支持lambda表达式,而函数式编程的大厦最初就是基于lambda计算构建起来的。函数式编程对于前端来说是必选项,后端则不必。

React框架的组件从很早开始就是不仅支持类式组件,也支持函数式的组件。而后React Hooks的出现,使得函数式编程思想越来越变得不可或缺。

无副作用

这是函数式编程的精髓所在。

无副作用的函数应该符合下面的特点:

  1. 要有输入参数。如果没有输入参数,这个函数拿不到任意外部信息,也就不用运行了。

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

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

数学函数就是如此的:

const sqr3 = function(x){return x * x * x;
}
console.log(sqr3(2));

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

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

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

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

组合函数

在会无副作用的函数之后,需要好的将这些函数组合起来。

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

/** 命令式 */
const sqr3 = function(x){if (typeof x === 'number'){return x * x * x;}return 0;
}/** 函数式 */
const isNum = x => typeof x === 'number';
console.log(sqr3(isNum("20")));

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

/** fn 作为 预处理函数 */
const sqr3 = function(fn, x){const y = fn(x);return y * y;
}const sqr3New = function(x){return sqr3(isNum,x);
}
console.log((sqr3New(2.2)));

​​​​容器封装函数能力

如果我们想给其他的函数也复用这个isNum的能力,可以封装一个容器对象来提供这个能力:


class MayBeNumber{constructor(x){this.x = x;}map(fn){if (isNum(this.x)){return MayBeNumber.of(fn(this.x));}return MayBeNumber.of(0);}getValue(){return this.x;}
}

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

const notnum = new MayBeNumber(undefined).map(Math.sin).getValue();
console.log(notnum);

可以发现,输出值从NaN变成了0。而且封装到对象中的另一个好处是可以用"."多次调用了。

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

const num = new MayBeNumber(1).map(Math.sin).map(sqr2).getValue();
console.log(num);

of 封装 new

上面的封装到对象中,但是函数式编程还搞出来new对象再map,最好是构造对象也是个函数。给它定义个 of 方法:

MayBeNumber.of = function(x){return new MayBeNumber(x);
}
const num = MayBeNumber.of(2).map(Math.tan).map(Math.exp).getValue();
console.log(num);

再来看下另一种情况,我们处理返回值的时候,如果有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(2, undefined).map(sqr3));// 输出结果为:Result { Ok: 8, Err: undefined }

这是一种容器的设计模式:

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

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

我们可以把这个设计模式叫做Functor函子。如果这个容器还提供一个of函数将值转换成容器,那么它叫做Pointed Functor。比如 JavaScript 中的Array类型。

简化对象层级

借助Result结构,对sqr3的返回值进行格式化。如果是数值的话,Ok是数值,Err是undefined。如果非数值的话,Ok是undefined,Err是0:

const sqr3Res = function(x){if (isNum(x)){return Result.of(x * x * x, undefined);}return Result.of(undefined, 0);
}console.log(Result.of(4.3, undefined).map(sqr3Res));
// 输出结果:Result { Ok: Result { Ok: 18.49, Err: undefined }, Err: undefined }

返回的是一个嵌套的结果,但是我们需要的是子Result的值。需要个Result 加一个 join函数:

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;}return this.Err;}flatMap(fn){return this.map(fn).join();}
}
Result.of = function(Ok, Err){return new Result(Ok, Err);
}console.log(Result.of(3, undefined).flatMap(sqr3Res));// 输出结果:Result { Ok: 27, Err: undefined }

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

偏函数和高阶函数

函数式编程与命令行编程体感上的最大区别:

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

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

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

如何用函数式方法实现一个只执行一次有效的函数?

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

const once = (fn) => {let done = false;return function() {return done ? undefined : ((done=true), fn.apply(this,arguments));}
}const initData = once(() => {console.log("Initialize data");}
);initData();
initData();

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

递归与记忆

递归是函数式编程中比较复杂的,最简单的递归就是阶乘:

const factorial = (n) => {if (n === 0){return 1;}return n * factorial(n - 1);
}console.log(factorial(10));

如此会重复计算好多次,效率较低,应该使用动态规划或者缓存记忆。没错,我们可以封装一个叫memo的高阶函数来实现这个功能:

const memo = (fn) => {const cache = {};return (arg) => cache[arg] || (cache[arg] = fn(arg));
}

使用memo的后阶乘:

 const fastFact = memo((n) => {if (n <= 0){return 1;}return n * fastFact(n-1);}
);

言归前端,React Hooks 中的 useMemo就是使用的这种记忆机制:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

总结

在日常使用中,需要记住这几点:

  1. 函数式编程的核心就是将函数存到变量里,用在参数里,用在返回值里;

  2. 在编程时要时刻记住将无副作用与有副作用代码分开;

  3. 函数式编程背后有其数学基础,不仅仅是一种设计模式。

前端开发之函数式编程相关推荐

  1. 前端工程化及函数式编程

    前端工程化 1.技术选型 vue.react.element.ant 2.统一规范 eslint.husky 3.测试.布署.监控 ut.e2e.mock 4.性能优化 lazy.module a.视 ...

  2. Web前端开发技术————期末编程例题

    一.计算梯形的面积.按下图所示页面效果,编程实现所需功能. (1)表单中设置4个文本框.1个按钮.1个重置按钮,其中梯形的面积文本框设置为只读: (2)编写两个自定义函数,分别是计算梯形的面积函数ar ...

  3. web前端开发技巧,编程前端开发

    近几年,随着技术和政策的变化,国内对技术人才的需求也是翻天覆地的变化. 为什么转行前端呢? 对于很多毕业学生来说,前端开发工程师行业需求大.待遇好.不限门槛.政策优,成为了很多大学生的就业发展选择. ...

  4. promise 浏览器实现的源码_【大前端01-01】函数式编程与JS异步编程、手写Promise...

    [简答题]一.谈谈你是如何理解JS异步编程的,EventLoop.消息队列都是做什么的,什么是宏任务.什么是微任务? 如何理解JS异步编程 众所周知JavaScript语言执行环境是"单线程 ...

  5. (送书和红包)快人一步,掌握前端函数式编程

    大家好,我是若川.上周末送出了3本新书和若干红包,抽奖名单已公布.本周又争取到了4本<前端函数式编程>书籍包邮送给大家,抽奖规则见文末,与以往不同的是除了关键词.留言.在看抽奖外,还有最早 ...

  6. JS函数式编程概念理解:函子(Functor)

    标签(空格分隔): 函数式编程 函子 functor 很多前端在学习函数式编程之前,都会被各种概念折磨的死去活来,本文的重点算是函数式编程之前的一个甜品,重点在如何切入. 函子即Functor是FP( ...

  7. 小白vue_web前端开发:新手学习前端应该先学vue还是react?

    新手学vue还是react?下面本篇文章给大家分析一下.有一定的参考价值,正在学习或者有需要的朋友可以参考一下,希望对大家有所帮助. 出身背景: react是facebook团队开发,2013.3月发 ...

  8. 前端搬运工:零基础的前端开发初学者应如何系统地学习?前端掌握技能的学习路线

    前端小伙伴们:[刚入门,但迷茫人群],请认真读完 下面的 淘宝web 大神总结,如果你对前端是真爱的话,并且坚信可以作为职业去改变你的生活,慢慢日积月累,按这个来吧,真的! 上半部分是 技术路线, 下 ...

  9. 前端搬运工 零基础的前端开发初学者应如何系统地学习 前端掌握技能的学习路线

    前端小伙伴们:[刚入门,但迷茫人群],请认真读完 下面的 淘宝web 大神总结,如果你对前端是真爱的话,并且坚信可以作为职业去改变你的生活,慢慢日积月累,按这个来吧,真的! 上半部分是 技术路线, 下 ...

最新文章

  1. 优秀!一鼓作气学会“一致性哈希”,就靠这 18 张图了
  2. 《Unity着色器和屏幕特效开发秘笈》—— 3.4 创建BlinnPhong高光类型
  3. <X86汇编语言:实模式到保护模式>四十六 中断和异常的处理与抢占式多任务
  4. CTFshow 命令执行 web42
  5. 智慧办公的AI博弈——看飞企互联如何接招!
  6. 自定义ClassLoader和双亲委派机制
  7. Android--Activity的跳转及Activity之间的数据传递
  8. Android Studio 设置/更改 SDK 路径
  9. ImportError: No module named apex
  10. 警惕!这5种“脸色”在暗示你这些健康问题!
  11. 信息安全管理的效益分析
  12. 多目标、多阶段、多层次的强化学习合作方法
  13. html css 浏览器 响应式 面试题持续更新!
  14. android显示大图片
  15. 双向链表list(十二)
  16. DP的一些杂题(思维型)
  17. 2022年9月全球数字安全最新新闻汇总
  18. 教你cad版本怎么用转换器转换操作
  19. `include “uvm_macros.svh“引发的思考
  20. 引入antd组件样式,按需引入的高级配置

热门文章

  1. 在linux中删除多级目录,如何在Linux中删除目录
  2. syslog 服务器过程详解
  3. iMeta | 2022年iMeta进展与审稿人致谢
  4. ssm框架搭建遇到的问题
  5. NFT 为何能够重塑艺术价值?
  6. OMV搭建系列教程[0] – 最小化安装Debian11
  7. 记一次omv的成功安装(避坑)
  8. 身份证阅读器读相片经常一片白色
  9. 家里装修吊顶转角怎么处理才不会开裂?
  10. Docker最新超详细教程——安装与部署