JavaScript函数式编程是一个由来已久的话题,但似乎从2016年开始,就越来越火了。这可能是因为ES6语法对函数式编程更友好,也可能是因为RxJS(ReactiveX)等函数式框架的流行。看过很多关于函数式编程的讲解,但大多是理论层面的,有的只是针对Haskell等纯函数式编程语言。而这篇文章旨在谈谈我眼中的JavaScript中函数式编程的具体实践。之所以是“inmyeyes”,是指我所说的仅代表我个人的观点,可能会与一些严格的概念相冲突。本文将省略很多形式化的概念介绍,重点介绍什么是JavaScript中的函数式代码,函数式代码和一般写法有什么区别,函数式代码能给我们带来什么好处,以及常见的一些函数式模型有哪些。我所理解的函数式编程我认为函数式编程可以理解为一种以函数为主要载体,用函数对一般表达式进行拆解和抽象的编程方法。与命令式相比,这样做有什么好处呢?主要有以下几点:更清晰的语义更高的复用性更高的可维护性有限的范围和更少的副作用基本的函数式编程下面的例子是一个具体的函数式体现//数组中的每个单词,首字母大写//一般写法constarr=['苹果','笔','苹果笔'];for(constiinarr){constc=arr[i][0];arr[i]=c。toUpperCase()+arr[i].slice(1);}console.log(arr);//函数式写法upperFirst(word){returnword[0].toUpperCase()+word.slice(1);}functionwordToUpperCase(arr){returnarr.map(upperFirst);}console.log(wordToUpperCase(['apple','pen','apple-pen']));//函数式写法2console.log(arr.map(['apple','pen','apple-pen'],word=>word[0].toUpperCase()+word.slice(1)));当事情变得复杂时,表达式的写法会出现几个问题:表达式不明显,逐渐变得难以维护。复用性差,会产生较多的代码和很多中间变量。函数式编程很好的解决了上述问题。首先参考函数式的写法1,使用函数封装将函数拆解(粒度不完善),封装成不同的函数,然后使用组合调用来达到目的。这样做使表达式清晰,易于维护、重用和扩展。其次,使用高阶函数,Array.map代替for...of做数组遍历,减少中间变量和操作。函数式写法1和函数式写法2的主要区别在于可以考虑后面是否可以复用该函数,如果不能的话,后者更好。链式优化我们从上面的函数式写法2可以看出,在写函数式代码的过程中,很容易造成水平扩展,即产生多层嵌套。下面我们举一个极端的例子。//计算数字之和//一般写法console.log(1+2+3-4)//函数式写法functionsum(a,b){returna+b;}functionsub(a,b){returna-b;}console.log(sub(sum(sum(1,2),3),4);这个例子只是水平扩展的极端情况。随着函数嵌套层数的增加,代码的可读性大大降低,容易出错。这种情况下,我们可以考虑多种优化方式,比如下面的链式优化。//优化写法(嗯,你没看错,这就是lodash链写)constutils={chain(a){this._temp=a;returnthis;},sum(b){this._temp+=b;returnthis;},sub(b){this._temp-=b;returnthis;},value(){const_temp=this._temp;this._temp=undefined;return_temp;}};console.log(utils.chain(1).sum(2).sum(3).sub(4).value());这样改写之后,整体结构会更加清晰,链条的各个环节是什么做的事情也可以很容易地展示出来。函数的嵌套和链接另一个比较好的例子是回调函数和Promise模式。//依次请求两个接口//回调函数import$from'jquery';$.post('a/url/to/target',(rs)=>{if(rs){$.post('a/url/to/another/target',(rs2)=>{if(rs2){$.post('a/url/to/third/target');}});}});//来自'catta'的Promiseimport请求;//catta是一个轻量级的请求工具,支持fetch、jsonp、ajax,无依赖request('a/url/to/target').then(rs=>rs?$.post('a/url/to/another/target'):Promise.reject()).then(rs2=>rs2?$.post('a/url/to/third/target'):Promise.reject());随着回调函数的嵌套层数和单层复杂度的增加,会变得臃肿难维护。但是Promise的链式结构在复杂度高的时候还是可以垂直扩展的,层次隔离非常清晰。常见的函数式编程模型闭包(Closure)可以使局部变量不被释放。它被称为闭包。闭包的概念比较抽象。相信大家或多或少都知道。包包能给我们带来什么好处?让我们看看如何创建一个闭包://创建一个闭包函数makeCounter(){letk=0;返回函数(){返回++k;};}constcounter=makeCounter();console.log(counter());//1console.log(计数器());//2makeCounter函数的代码块,在返回的函数中,引用了局部变量k,导致局部变量无法被系统回收,导致关闭。这个闭包的作用是“保留”局部变量,使得调用内部函数时可以重用该变量;与全局变量不同,变量只能在函数内部引用。换句话说,闭包实际上创建了一些函数私有的“持久变量”。所以从这个例子,我们可以得出创建闭包的条件是:有两层函数:内层函数和外层函数内层函数引用外层函数的局部变量闭包的目的闭包的主要目的就是定义一些函数域限定的持久化变量,这些变量可以用于缓存或者中间计算等。//简单缓存工具//匿名函数创建闭包constcache=(function(){conststore={};return{get(key){returnstore[key];},set(key,val){store[key]=val;}}}());cache.set('a',1);cache.get('A');//1上面的例子是一个简单的缓存工具的实现,匿名函数创建一个闭包,这样store对象就可以一直被引用,不会被回收。闭包的缺点持久化变量不会被正常释放,持续占用内存空间,很容易造成内存浪费,所以一般需要一些额外的手动清理机制。高阶函数接受或返回函数的函数称为高阶函数。听起来是个很冰冷的词,但其实我们经常用到,只是不知道他们的名字。JavaScript语言本身就支持高阶函数,因为JavaScript函数是一等公民,既可以用作另一个函数的参数,也可以用作另一个函数的返回值。我们经常可以在JavaScript中看到很多原生的高阶函数,比如Array.map、Array.reduce、Array.filter。我们以地图为例。让我们看看他是如何使用地图(map)的。Mapping是针对collections的,也就是说,集合中的每一项都以同样的方式进行转化,生成一个新的setmap作为一个高阶函数,它接受一个函数参数作为mapping的逻辑//给每一项加一在数组中形成一个新的Array//一般写法constarr=[1,2,3];constrs=[];for(constnofarr){rs.push(++n);}console.log(rs)//映射重写constarr=[1,2,3];constrs=arr.map(n=>++n);上面一般的写法,使用for...of循环遍历数组会产生额外的操作,存在改变原数组的风险,而map函数封装了必要的操作,这样我们只需要关心映射逻辑的功能实现,减少代码量和副作用的风险。Currying给定一个函数的一些参数,生成一个接受其他参数的新函数可能不会经常听到这个名词,但是用过undescore或者lodash的人都见过他。有一个神奇的_.partial函数,就是柯里化的实现//获取目标文件相对于base路径的相对路径//一般写法constBASE='/path/to/base';constrelativePath=path.relative(BASE,'/some/path');//_.parical重写constBASE='/path/to/base';constrelativeFromBase=_.partial(path.relative,BASE);constrelativePath=relativeFromBase('/some/path');通过_.partial,我们得到了一个新的函数relativeFromBase,相当于调用时调用了path.relative,并且默认将第一个参数传递给BASE,后续参数的传递顺序为place。在这个例子中,我们真正想要完成的是每次获取相对于BASE的路径,而不是相对于任何路径。柯里化可以让我们只关心函数的一些参数,让函数的目的更明确,调用更简单。Composing结合了多个函数的能力来创建一个新的函数。你第一次见到他可能是在lodash中。compose方法(现在称为flow)//将数组中的每个单词大写并进行Base64//一般写法(其中之一)constarr=['pen','apple','applypen'];constrs=[];for(constwofarr){rs.push(btoa(w.toUpperCase()));}console.log(rs);//_.flow重写constarr=['pen','apple','applypen'];constupperAndBase64=_.partialRight(_.map,_.flow(_.upperCase,btoa));console.log(upperAndBase64(arr));_.flow结合了转大写和转Base64的功能,生成一个新的功能。方便作为参数函数或者后续复用。在我看来,我所理解的JavaScript函数式编程可能与许多传统概念不同。我不只是认为高级函数算作函数式编程。其他的,比如普通的函数组合调用,链式结构等等,我觉得都属于函数式编程的范畴,只要是以函数为主要载体的。而且我认为函数式编程不是必须的,也不应该是强制性的要求或要求。它是其中一种方式以及面向对象或其他想法。更多的情况下,我们应该是几种的结合,而不是局限于概念。
