前言在说函数式组件之前,我需要声明一下,我对函数式编程知之甚少。写这篇文章都是因为函数式编程在前端开发比较流行的今天。这就是我要学习Orz的原因。像ES6中的箭头函数,Redux中的compose,React16.6之后的React.memo(),16.8之后的Hooks,函数式组件的使用已经成为主流(自然也有Vue3中的compositionAPI等),因为各种我们使用的框架,越来越倾向于函数式开发,所以为了理解和使用这些代码,我们需要学习函数式编程什么是函数式编程?写代码的时候,写法不同,流派也就不同。主流的编程范式有两种,那就是命令式编程(Imperativeprogramming)和声明式编程(Declarativeprogramming),当然还有事件驱动编程等等,这里就不多说了。在命令式编程语言中,一般支持四种基本语句:操作语句循环语句(for,while)条件分支语句(ifelse,switch)无条件分支语句(return,break,continue)它关注的是过程,所以它也是称为过程式编程,对应于声明式编程命令式编程告诉计算机如何计算,关心解决问题的步骤声明式编程告诉计算机需要计算什么,关心解决问题的目标这两种编程方式都在开花JavaScript。在以往的开发中(也就是在React、Vue等没有出现之前),我们习惯使用命令式编程,其代表是面向对象编程,通过对象、类等特性来实现面向对象编程、继承和封装。流行的库是jquery。尽管一开始并不那么流行,但JavaScript一直支持声明式编程,即通过函数式编程来实现,因为函数是一等公民,可以作为参数和返回值。流行的库是underscorejs和lodash。函数式编程的特点是什么?Currying、缓存函数、纯函数、函数组合(compose)、不可变数据和高阶函数。什么是柯里化?柯里化是一种将接受多个参数的函数转换为接受单个参数(原始函数的第一个参数)的函数并返回一个接受剩余参数并返回结果的新函数的技术——换句话说,柯里化是函数转换,将一个函数从f(a,b,c)转换为f(a)(b)(c)一般情况下:使用闭包保存参数,当参数个数足够执行函数时,开始执行函数。它不调用函数,它只是在柯里化之前转换函数//functionadd(x,y){returnx+y;}add(1,2);//3//在柯里化之后functionadd(y){returnfunction(x){returnx+y;};}//ES6箭头函数表示constadd=x=>y=>x+y;add(2)(1);如上例,我们将add(1,2)转换为add(2)(1)。当然,这个case是为了模拟currying。一般来说,我们会写一个“转换函数”,让函数适配currying函数constcurryAdd=curry(add)console.log(curryAdd(1)(2))//3那么curry怎么实现呢?判断当前函数传入的参数是否大于等于fn需要的参数个数,需要满足两点。如果是,直接执行fn。如果传递的参数个数不够,则返回一个闭包,将传递的参数暂存,返回curry函数constcurry=(fn,...args)=>{if(args.length>=fn.length){returnfn(...args)}else{return(...args2)=>curry(fn,...args,...args2)}}让我们用一个简单的例子来验证它:functionfun(a,b,c){returna+b+c;}constcurryFun=curry(fun);curryingFun(1)(2)(3);//6curryingFun(1,2)(4);//7curryingFun(1,2,5);//8看函数式编程概念的两个库:undersore.js和curryundersore.jslodashlodash中实现的箭头函数的使用在上面的例子中,我们经常使用箭头函数。箭头函数的魅力之一就是符合函数式编程的思想,例如:functionadd(y){returnfunction(x){r返回x+y;};}constadd=x=>y=>x+y;//在redux中组合函数constcompose=(...func)=>{if(func.length===0){returnargs=>args}if(func.length===1){returnfunc[0]}returnfunc.reduce((a,b)=>(...args)=>a(b(...args)))}functionenhancer(originF){返回函数(...args){console.log('before');常量结果=originF(...args);console.log('之后');返回结果;};}//等于constenhancer=(originF)=>(...args)=>{console.log('before');常量结果=originF(...args);console.log('之后');返回结果;};//三数相乘constmulti=a=>b=>c=>a*b*c作者认为柯里化就是利用闭包的特性来保存参数然后,想用的时候释放it,还有一个官方的curriedapplication是在开发curriedapplication的时候,我们会遇到这种情况,开发一个工具函数,在调用的时候,会用到一个Frequent参数,如果把这个参数提取出来,代码可以优化,例如functionadd(a,b){returna+b}add(10,1)//11add(10,11)//21add(10,111)//121如果我们柯里化并构建第一个参数10进函数,代码就清晰了//最简单的优化函数add10(b){returnadd(10,b)}add10(1)//11add10(11)//21add10(111)//121函数add10是柯里化函数,正如我们上面所说的,包装函数一般用来适配成柯里化函数,这里我们调用上面的柯里函数//完整版函数add(a,b){returna+b;}//柯里化包装函数constcurry=(fn,...args)=>{if(args.length>=fn.length){returnfn(...args)}else{return(...args2)=>curry(fn,...args,...args2)}}constadd10=curry(add,10)add10(1)//11add10(11)//21add10(111)//121缓存函数cache函数只是缓存返回值的一种方式一个函数的实现与柯里化相比,它似乎更简单。例如,functionadd(a,b){returna+b;}letmemoAdd=memoize(add)memoAdd(1,2)//3memoAdd(1,3)//同样的参数,第二次调用,从cache数据怎么取出来?其实很简单,就是用闭包存储在内存中。一致则返回,不一致则存入对象,然后返回constmemoize=(func)=>{letcache={}returnfunction(key){if(!cache[键]){缓存[键]=func.apply(this,arguments)}returncache[key]}}纯函数作者认为纯函数类似于数学中的函数,比如y=f(x),更具体地说:y=2x,同样的输入,有相同的输出,执行过程中没有副作用所以它有两个特点:不改变原来的数据(没有副作用)返回一个新的数据constarr=[1,2,3]//纯函数arr.slice(1)console.log(arr)//[1,2,3]//产生副作用,不纯函数arr.splice(1)console.log(arr)//[1]JavaScript中哪些API是纯函数arrays:slice,concat,map,reduce,filterslice,concat我们在copy的秘诀里说过,它是一种浅拷贝方法map,filter是ES6中新的APIstring:String类型,其他基本类型的API都没有allowedChange,他们的每个API都是纯函数,不会改变原来的数据varstr="hello"console.log(str.slice(0,3))//hel,slice也适用于String类型,控制台.log(str)//helloconsole.log(str.substr(0,3))//helconsole.log(str)//helloconsole.log(str.substring(0,3));//helconsole.log(str)//hello//其他API同理。函数组合(compose)就是依次执行多个函数,将这些函数组合起来,自动依次执行。这个过程就是函数组合。这样做的目的是让代码更简单易读functiondouble(num){returnnum*2}functionsquare(num){returnnum**2}//常见做法constres=square(double(10))console.log(res)//400//函数组合functioncomposeFn(fn1,fn2){returnfunction(count){returnfn2(fn1(count))}}constnewFn=composeFn(double,square)constres2=newFn(20)//1600看下划线的compose函数functioncompose(){varargs=争论;varstart=args.length-1;返回函数(){vari=开始;varresult=args[start].apply(this,arguments);while(i--)结果=args[i]。调用(这个,结果);返回结果;}}看看compose是不是眼熟,redux中的源码是这样写的=1){returnfuncs[0]}返回函数。reduce((a,b)=>(...args)=>a(b(...args)))}如果在实际开发中,这个概念可以用来解决洋葱代码fn3(fn2(fn1(fn0(x))))//洋葱代码varresult=compose(f3,f2,f1,f0)//函数组合代码result(x)高阶函数简单来说,高阶函数就是返回另一个的函数函数函数sayHello(){returnfunction(){console.log("hello")}}本质是函数是一等公民,满足React中高阶组件作为参数或返回值的概念,原则是相同。即传入一个组件作为参数,返回另一个组件Immutabledata不可变数据结构函数式编程中的基本原则JavaScript对象是可变的,原因是为了节省内存,但同时也带来了副作用,当应用程序后是复杂,维护代码的难度会增加。我们在复制的秘密中说过,基本类型的数据是不可变的,因为它们存在于栈内存中,而引用类型(即对象)的数据是可变的。它存在于Heap内存中,在赋值的时候,拷贝的时候是它的引用地址,所以我们在赋值的时候会根据情况使用浅拷贝和深拷贝。在开发React时,我们会使用immutable.js和immer来使数据不可变。总结一下我们除此之外,函数式编程单独在前端有什么用?比如Koa中的onion模型和redux中的compose函数就是函数组合。React开发中的HOC高层组件使用了高层函数,它(React)可以引用不可变的数据库来保持数据的唯一性。(immutable.js,immer)是函数式编程的特性——不可变数据。当然,这些只是笔者对函数式编程的浅薄理解。当然,以上几点我是不会的,还有lazyfunctions,functor等知识点,还有其他语言的函数式编程,等作者水平提高了再上阵。参考简明JavaScript函数式编程-简介编程的真正用处在哪里?JavaScript专题函数组合系列文章深入理解JavaScript——入门深入理解JavaScript——什么是JavaScript深入理解JavaScript——JavaScript由什么组成深入理解JavaScript——万物皆对象———whatdidnewJavaScript深入理解——Object.createJavaScript深入理解——copy的秘密JavaScript深入理解——PrototypeJavaScript深入理解——Inheritance深入理解JavaScript——JavaScript中的第一位皇帝深入理解JavaScript——instanceof——寻祖深入理解JavaScript——Function深入理解JavaScript——Scope深入理解ofJavaScript——this关键字深入理解JavaScript——call,apply深入理解JavaScript——立即调用函数(IIFE)深入理解JavaScript——词法环境深入理解JavaScript——ExecutionContextandCallStack深入理解JavaScript——ScopeVSExecutionContext深入理解JavaScript——闭包深入理解JavaScript——防抖与节流深入理解JavaScript——函数式编程深入理解JavaScript——垃圾回收机制深入理解JavaScript——数组深入理解JavaScript——循环来了
