编程范式
在介绍函数式编程前,先来了解一下编程范式。
编程范式首次被提出是在 1978 年 Robert Floyd 的图灵奖颁奖演说(The Paradigms of Programming),是指程序的设计方法,即程序应该如何被构建和执行。
主要的编程范式有结构化编程、面向对象编程和函数式编程。
结构化编程范式是将解决问题的方法抽象为一系列步骤。面向对象编程范式借助封装、继承和多态,将程序设计成对象和对象的交互。
函数式编程
函数式编程范式的思想是最小化变化点。
OO makes code understandable by encapsulating moving parts. FP makes code understandable by minimizing moving parts.
— Michael Feathers
这体现在尽可能少的数据结构,尽可能多的操作(Few Data Structures, Many Operations):
- 将程序看成表达式和转换的组合 。
- 用函数完成计算和转换:
- 高阶函数:函数可以作为参数和返回值;
- 纯函数:不修改外部变量的值。
- 闭包:函数使用的变量构成一个独立的上下文环境。
- 惰性计算。
- 递归。
- 不可变的数据:不改变输入数据,产生新的输出数据。
- 柯里化:只传递给函数一部分参数来调用函数,然后返回一个函数去处理剩下的参数。
TypeScript 与函数式编程
按照上一节的标准 TypeScript 或者说 JavaScript 是支持函数式编程的,即使是标准库数组中不支持的惰性计算也可以用 Generator 实现。以下介绍几个函数式编程常见的三个操作:
过滤
filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
过滤操作产生一个新的,由使谓词函数返回 true 的数组元素组成的数组。
const emojis = ['🥵', '😁', '🥵']
const hots = emojis.filter((emoj) => emoj === '🥵')
console.log(hots) // ['🥵', '🥵']
转换
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
转换操作产生一个新的,由数组元素作为参数的回调函数返回值组成的数组。
const emojis = ['🥵', '😁', '🥵']
const hots = emojis.map((emoj) => emoj !== '🥵' ? '🥵' : emoj)
console.log(hots) // ['🥵', '🥵', '🥵']
递归
reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
递归操作产生一个新值。该操作为数组中的所有元素调用回调函数,函数的返回值在下次调用回调函数时作为参数提供。递归操作返回的新值就是回调函数返回值的累计结果。
const emojis = ['🥵', '😁', '🥵']
const hotsCnt = emojis.reduce((cnt, emoj) => emoj === '🥵' ? cnt + 1 : cnt, 0)
console.log(hotsCnt) // 2
总结
每种编程范式都有其用武之地。无论是面向对象还是函数式编程范式,都是在践行 GRASP 中的变换保护原则:识别变化点,并将其保护起来。函数式的优点在于高级函数和纯函数不产生副作用,也不修改参数值,保证了代码可维护性和可调式性。
React 中就特别推崇纯函数,开发者需要保证相同的条件下渲染结果是一致,个人觉得这种范式极大降低了心智负担。但函数式编程并不能解决所有问题,许多库的出现就是为了解决 React 中这一部分问题。