函数式编程实际上比较复杂,所以本文也只是泛泛而谈,简单聊聊函数式编程特点以及函数式编程思维在js日常开发中的体现
函数式编程特点:
函数是“一等公民”
这点在js中体现得很明显了,函数是“一等公民”意味着意味着函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
比如回调函数就是将函数作为参数传入另一个函数,js的闭包特性可以让我们将函数作为另一个函数的返回值,从而保持对外层函数作用域的引用
JSer都知道的定时器回调函数:
1 | setTimeout(() => { console.log(111) }, 0) |
JSer都了解的闭包:
1 | function foo() { |
声明式编程
声明式编程意味着告诉程序需要做什么,而非怎么做,声明式编程可读性较高,同时开发效率相比于命令式编程更高,因为声明式编程解放了人力,使得开发者不需关心具体的实现。比如SQL语句就是声明式的,前端的Vue和React也是声明式的
考虑如下例子:我们有一个对象数组,现在我们要给里面的每个对象添加一个属性“gender”
命令式编程是这样的:
1 | const arr = [{ |
声明式编程是这样的:
1 | const newArr = arr.map(item => ({ ...item, gender: 'male' })) |
这里的两种方式还有一点不同,命令式编程中我们手动遍历了arr,为它的每一项添加了”gender”属性。这样一番操作之后,原来的数组arr是被改变了的,而使用map方法后,原来的arr并没有改变,只是返回了一个我们需要的新数组。不改变数据的值是函数式编程中非常重要的特点,这点后面会讨论
惰性执行
惰性执行指的是函数只在需要的时候执行,即不产生无意义的中间变量
无状态和数据不可变
无状态和数据不可变是函数式编程核心概念
无状态主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化
考虑下面这个函数:
1 | function getSum(a, b) { |
这个函数显然满足无状态的特点,无论何时运行,给定相同的输入,永远给出相同的输出
数据不可变主要是要求所有的数据都是不可变的,拿上面给数组中的对象添加“gender”属性的例子来说,使用for of 循环的方法为每个对象都新增了一个“gender”属性,操作结束后,arr的值也改变了;而使用map操作仅仅是返回了一个新的数组,原数组arr并没有被改变
为了实现无状态和数据不可变,函数式编程提出了函数应该具有的特性:没有副作用和纯函数
没有副作用
副作用的含义是函数在完成主要作用之外完成的其他副要功能
在我们日常开发中,函数最主要的功能当然是根据输入返回结果,而在函数中我们最常见的副作用就是随意操纵外部变量
为什么函数式编程要求没有副作用呢,这是因为保证函数没有副作用,一来能保证数据的不可变性,二来能避免很多因为共享状态带来的问题。当一个人维护代码时候可能还不明显,但随着项目的迭代,项目参与人数增加,大家对同一变量的依赖和引用越来越多,这种问题会越来越严重。最终可能连维护者自己都不清楚变量到底是在哪里被改变而产生 Bug。
纯函数
纯函数的的概念很简单,就两点:
1.不依赖外部状态(无状态):函数的的运行结果不依赖全局变量,this 指针,IO 操作等;
2.没有副作用(数据不变):不修改全局变量,不修改入参
其实就是上面已经谈过的两点,无状态和数据不变的特点使得一个函数成为了纯函数,这意味着对于相同的输入,该函数永远只会给出相同的输出
考虑以下例子,下面两个函数就不是纯函数:
1 | const obj = { |
函数sayHello不是纯函数的原因是它的执行结果依赖于外部变量,当obj值不同时,sayHello的执行结果也就变了。函数changeName不是纯函数的原因是它修改了传入参数
这个功能纯函数的写法是怎样的呢?考虑下面的例子:
1 | const obj = { |
上面介绍了函数式编程的一些概念,现在总结下函数式编程的优缺点:(以下内容为复制粘贴)
优点:
1.代码简洁,开发快速:函数式编程大量使用函数的组合,函数的复用率很高,减少了代码的重复,因此程序比较短,开发速度较快。Paul
Graham 在《黑客与画家》一书中写道:同样功能的程序,极端情况下,Lisp 代码的长度可能是 C 代码的二十分之一。
2.接近自然语言,易于理解:函数式编程大量使用声明式代码,基本都是接近自然语言的,加上它没有乱七八糟的循环,判断的嵌套,因此特别
易于理解。
3.易于”并发编程”:函数式编程没有副作用,所以函数式编程不需要考虑“死锁”(Deadlock),所以根本不存在“锁”线程的问题。
4.更少的出错概率:因为每个函数都很小,而且相同输入永远可以得到相同的输出,因此测试很简单,同时函数式编程强调使用纯函数,没有副作
用,因此也很少出现奇怪的 Bug。
因此,如果用一句话来形容函数式编程,应该是:Less code, fewer bugs 。因为写的代码越少,出错的概率就越小。人是最不可靠的,我们应
该尽量把工作交给计算机。
缺点:
一眼看下来好像函数式可以解决所有的问题,但是实际上,函数式编程也不是什么万能的灵丹妙药。正因为函数式编程有以上特点,所以它天生就有
以下缺陷:
1.性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销。同时,
在 JS 这种非函数式语言中,函数式的方式必然会比直接写语句指令慢(引擎会针对很多指令做特别优化)。就拿原生方法 map 来说,它就要比纯
循环语句实现迭代慢 8 倍。
2.资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收(Garbage Collection)所产生的压力远远超
过其他编程方式。这在某些场合会产生十分严重的问题。
3.递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作,为了减少递归的性能开销,我们往往会把递归写成尾递归形式,以便让解析
器进行优化。因此,在性能要求很严格的场合,函数式编程其实并不是太合适的选择
参考资料:https://zhuanlan.zhihu.com/p/81302150
更多链接:
b站上一个介绍函数式编程的视频,初探函数式编程(JavaScript篇):
https://www.bilibili.com/video/BV1bE411B74p?from=search&seid=16469932293357359141&spm_id_from=333.337.0.0