最近通过一些机缘巧合,有接触到一系列的函数式编程与高阶函数一类的编程。花了点时间整理
编程范式
- 命令式
- 声明式
- 面向对象
- 函数式
函数式编程
函数式编程 (通常简称为 FP)是指通过复合纯函数来构建软件的过程。
它避免了共享的状态(share state)、易变的数据(mutable data)
、以及副作用(side-effects)
函数式编程是声明式而不是命令式,并且应用程序状态通过纯函数流转。对比面向对象编程,后者的应用程序状态通常是共享并共用于对象方法。
这是一种编程范式
如果你想要了解函数式编程在实际中的意义,你需要从理解那些核心概念开始:
- 纯函数(Pure functions)
- 函数复合(Function composition)
- 避免共享状态(Avoid shared state)
- 避免改变状态(Avoid mutating state)
- 避免副作用(Avoid side effects)
纯函数:
纯函数有一下的特征
- 同样的输入,返回同样的结果
- 没有副作用
- 不依赖外部状态
函数复合
是结合两个或多个函数,从而产生一个新函数或进行某些计算的过程。
例如,复合操作 f·g(点号意思是对两者执行复合运算)在 JavaScript 中相当于执行 f(g(x))
共享状态
共享状态 的意思是任意变量、对象或者内存空间存在于共享作用域下,或者作为对象的属性在各个作用域之间被传递。共享作用域包括全局作用域和闭包作用域。
通常,在面向对象编程中,对象以添加属性到其他对象上的方式在作用域之间共享。
不可变性
不可变(immutable) 对象是指一个对象不会在它创建之后被改变。对应地,一个可变的(mutable)对象是指任何在创建之后可以被改变的对象。
副作用
副作用是指除了函数返回值以外,任何在函数调用之外观察到的应用程序状态改变。
副作用包括:
- 改变了任何外部变量或对象属性
- 写日志
- 在屏幕输出
- 写文件
- 发网络请求
- 触发任何外部进程
- 调用另一个有副作用的函数
在函数式编程中,副作用被尽可能避免,这使得程序的作用更容易理解,也使得程序更容易被测试。
你现在需要做的是要从你的软件中隔离副作用行为。如果你让副作用与你的程序逻辑分离,你的软件将会变得更易于扩展、重构、调试、测试和维护。
这也是为什么大部分前端框架鼓励我们分开管理状态和组件渲染,采用松耦合的模型。
区别
函数式编程倾向于复用一组通用的函数功能来处理数据。
面向对象编程倾向于把方法和数据集中到对象上。那些被集中的方法只能用来操作设计好的数据类型,通常是那些包含在特定对象实例上的数据。
命令式 && 声明式
函数式编程是一个声明式范式,意思是说程序逻辑不需要通过明确的描述控制流程来表达
命令式 程序花费大量代码描述所其外结果的步骤——控制流:如何做
声明式 程序抽象了控制流程的过程,花费大量代码描述数据流:即做什么
举个例子,下面是一个用 命令式 方式实现的 mapping 过程,接收一个数值数组,并返回一个新的数组,新数组将原数组的每个值乘以 2:
1 | const doubleMap = numbers => { |
而实现同样功能的 声明式 mapping 用函数 Array.prototype.map()
将控制流抽象了,从而我们可以表达更清晰的数据流:
1 | const doubleMap = numbers => numbers.map(n => n * 2); |
命令式 代码中频繁使用语句。语句是指一小段代码,它用来完成某个行为。通用的语句例子包括 for
、if
、switch
、throw
,等等……
声明式 代码更多依赖表达式。表达式是指一小段代码,它用来计算某个值。表达式通常是某些函数调用的复合、一些值和操作符,用来计算出结果值。
以下都是表达式:1
2
32 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
通常在代码里,你会看到一个表达式被赋给某个变量,或者作为函数返回值,或者作为参数传给一个函数。在被赋值、返回或传递之前,表达式首先被计算,之后它的结果值被使用。
结论
函数式编程偏好:
- 使用纯函数而不是使用共享状态和副作用
- 让可变数据成为不可变的
- 用函数复合替代命令控制流
- 使用高阶函数来操作许多数据类型,创建通用、可复用功能取代只是操作集中的数据的方法。
- 使用声明式而不是命令式代码(关注做什么,而不是如何做)
- 使用表达式替代语句
- 使用容器与高阶函数替代多态