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

本文探讨了在前端开发中,useState和useEffect等Hook的使用,强调了函数式编程在处理副作用和纯函数的重要性。同时,通过比较Haskell中的Monad概念,解释了在IO操作中为何需要函数式工具。作者提倡先掌握无副作用函数,再逐步理解函数式设计模式如Functor和Monad,以提升代码的可复用性和测试性。
摘要由CSDN通过智能技术生成

function Example() {

const [count, setCount] = useState(0);

// Similar to componentDidMount and componentDidUpdate:

useEffect(() => {

// Update the document title using the browser API

document.title = You clicked ${count} times;

});

return (

You clicked {count} times

<button onClick={() => setCount(count + 1)}>

Click me

);

}

那么,useState, useEffect之类的API跟函数式编程有什么关系呢?

我们可以看下useEffect的API文档:

Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.

Instead, use useEffect. The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.

所有的可变性、消息订阅、定时器、日志等副作用不能使用在函数组件的渲染过程中。useEffect就是React纯函数世界与命令式世界的通道。

当我们用React写完了前端,现在想写个BFF的功能,发现serverless也从原本框架套类的套娃模式变成了一个功能只需要一个函数了。下面是阿里云serverless HTTP函数的官方例子:

var getRawBody = require(‘raw-body’)

module.exports.handler = function (request, response, context) {

// get requset header

var reqHeader = request.headers

var headerStr = ’ ’

for (var key in reqHeader) {

headerStr += key + ‘:’ + reqHeader[key] + ’ ’

};

// get request info

var url = request.url

var path = request.path

var queries = request.queries

var queryStr = ‘’

for (var param in queries) {

queryStr += param + “=” + queries[param] + ’ ’

};

var method = request.method

var clientIP = request.clientIP

// get request body

getRawBody(request, function (err, data) {

var body = data

// you can deal with your own logic here

// set response

var respBody = new Buffer(‘requestHeader:’ + headerStr + ‘\n’ + 'url: ’ + url + ‘\n’ + 'path: ’ + path + ‘\n’ + 'queries: ’ + queryStr + ‘\n’ + 'method: ’ + method + ‘\n’ + 'clientIP: ’ + clientIP + ‘\n’ + 'body: ’ + body + ‘\n’)

response.setStatusCode(200)

response.setHeader(‘content-type’, ‘application/json’)

response.send(respBody)

})

};

虽然没有需要关注副作用之类的要求,但是既然是用函数来写了,用函数式思想总比命令式的要好。

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

如果在网上搜“如何学习函数式编程”,十有八九会找到要学习函数式编程最好从学习Haskell开始的观点。

然后很可能你就了解到那句著名的话”A monad is just a monoid in the category of endofunctors, what’s the problem?“。

翻译过来可能跟没翻译差不多:”一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已“。

别被这些术语吓到,就像React在纯函数式世界外给我们提供了useState, useEffect这些Hooks,就是帮我们解决产生副作用操作的工具。而函子Functor,单子Monad也是这样的工具,或者可以认为是设计模式。

Monad在Haskell中的重要性在于,对于IO这样虽然基础但是有副作用的操作,纯函数的Haskell是无法用函数式方法来处理掉的,所以需要借助IO Monad。大部分其它语言没有这么纯,可以用非函数式的方法来处理IO之类的副作用操作,所以上面那句话被笑称是Haskell用户群的接头暗号。

有范畴论和类型论等知识做为背景,当然会有助于从更高层次理解函数式编程。但是对于大部分前端开发同学来讲,这笔技术债可以先欠着,先学会怎么写代码去使用可能是更好的办法。前端开发的计划比较短,较难有大块时间学习,但是我们可以迭代式的进步,最终是会殊途同归的。

先把架式练好,用于代码中解决实际业务问题,比被困难吓住还停留在命令式的思想上还是要强的。

函数式编程的精髓:无副作用

57e51e58cd43649e0c463b080bbd0b77.png

前端同学学习函数式编程的优势是React Hooks已经将副作用摆在我们面前了,不用再解释为什么要写无副用的代码了。

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

  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函数一次性处理掉:

下面是我在学习HTML和CSS的时候整理的一些笔记,有兴趣的可以看下:

HTML、CSS部分截图

进阶阶段

进阶阶段,开始攻 JS,对于刚接触 JS 的初学者,确实比学习 HTML 和 CSS 有难度,但是只要肯下功夫,这部分对于你来说,也不是什么大问题。

JS 内容涉及到的知识点较多,看到网上有很多人建议你从头到尾抱着那本《JavaScript高级程序设计》学,我是不建议的,毕竟刚接触 JS 谁能看得下去,当时我也不能,也没那样做。

我这部分的学习技巧是,增加次数,减少单次看的内容。就是说,第一遍学习 JS 走马观花的看,看个大概,去找视频以及网站学习,不建议直接看书。因为看书看不下去的时候很打击你学下去的信心。

然后通过一些网站的小例子,开始动手敲代码,一定要去实践、实践、实践,这一遍是为了更好的去熟悉 JS 的语法。别只顾着来回的看知识点,眼高手低可不是个好习惯,我在这吃过亏,你懂的。

1、JavaScript 和 ES6

在这个过程你会发现,有很多 JS 知识点你并不能更好的理解为什么这么设计,以及这样设计的好处是什么,这就逼着让你去学习这单个知识点的来龙去脉,去哪学?第一,书籍,我知道你不喜欢看,我最近通过刷大厂面试题整理了一份前端核心知识笔记,比较书籍更精简,一句废话都没有,这份笔记也让我通过跳槽从8k涨成20k。

JavaScript部分截图

2、前端框架

前端框架太多了,真的学不动了,别慌,其实对于前端的三大马车,Angular、React、Vue 只要把其中一种框架学明白,底层原理实现,其他两个学起来不会很吃力,这也取决于你以后就职的公司要求你会哪一个框架了,当然,会的越多越好,但是往往每个人的时间是有限的,对于自学的学生,或者即将面试找工作的人,当然要选择一门框架深挖原理。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

以 Vue 为例,我整理了如下的面试题。

Vue部分截图

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值