函数式编是一种编程范式,一种编写计算机程序的风格。类似面向有对象有封装,继承,多态的概念,函数式编程也有自己的一些编程范式。下面简单介绍 JavaScript 的函数式编程范式,了解了下面的范式后,你可以说自己已经入门。
高阶函数 (Higher order function)
满足下面两个条件至少一个就算是高阶函数:
- 接收一个函数作为参数 (Accept a function as a argument)
- 返回一个函数 (Returns a function)
const withCount = fn => {
let count = 0
return (...args) => {
count += 1
console.log(`fn being called ${count} times`)
return fn(...args)
}
}
const add3 = x => x + 3
const countedAdd3 = withCount(add3)
countedAdd3(3) // fn being called 1 times
countedAdd3(3) // fn being called 2 times
countedAdd3(3) // fn being called 3 times
不可变数据 (Immutable data)
Mutable data 在创建后可以改变,Imutable data 在创建后不允许改变,函数式编程一般使用不可变数据,数据一旦创建后,不允许修改,只能替换。
// mutable example
let a = [1, 2, 3]
a.push(4)
console.log(a, 'a') // [ 1, 2, 3, 4 ] 'a'
// immutable example
let b = [1, 2, 3]
const immutablePush = (arr, arg) => [...arr, arg]
let c = immutablePush(b, 4)
console.log(c, 'c') // [ 1, 2, 3, 4 ] 'c'
console.log(b, 'b') // [ 1, 2, 3 ] 'b'
柯里化 (Currying)
柯里化指的是将多个参数的函数变成多个一个参数的函数的过程
// 两个参数的函数
const add = (x, y) => x + y
const r = add(1, 2)
console.log(r) // 3
// 多个一个参数的函数
const curryingAdd = x => y => x + y
const add2 = curryingAdd(2) // 可以复用
const rr = add2(1) // 3
const rr2 = add2(2) // 4
纯函数 (Pure Function)
纯函数类似数学范畴的函数定义
f(x) = x + 1
同时满足下面三个条件才叫做纯函数
- output 只依赖 input
- 同样的 input 会得到同样的 output
- 并且不产生任何副作用
// 不是纯函数的例子
// 依赖外部变量
let num = 1
const addByCount = x => x + count
// 同样的输入,不同输出
const addByRandom = x => x + Math.random() * 10
// 产生 side effect
let count = 0
const addOne = x => {
count += 1
return x + 1
}
// 产生 side effect
const addOneWithConsole = x => {
console.log(x)
return x + 1
}
// 纯函数
const pureFunc = x => x + 1
管道/组合 (Pipe / Compose)
多个函数按顺序处理一个数据的时候写起来会很丑(包很多层),pipe / compose 可以将多个函数合成一个函数,从而简化逻辑,看起来更有秩序
const text = 'hello world'
// 三个函数按顺序操作一个变量
const toUpper = text => text.toUpperCase()
const addExclamation = text => `${text}!`
const double = text => `${text}${text}`
const r = double(addExclamation(toUpper(text)))
console.log(r, 'r')
// 同样操作,改用 pipe 函数的方式
const pipe = (...fns) => (arg) => fns.reduce((sum, fn) => fn(sum), arg)
const rr = pipe(toUpper, addExclamation, double)(text)
console.log(rr, 'rr')
函数式编程的 Debug 方式
如果都是纯函数,debug 也是很方便的一件事,我们可以在 funtion 流处理数据的中间加上"埋点",比如下面的log()
函数
const text = 'hello world'
const toUpper = text => text.toUpperCase()
const addExclamation = text => `${text}!`
const double = text => `${text}${text}`
// logger
const log = msg => x => {
console.log(msg, x)
return x
}
const pipe = (...fns) => (arg) => fns.reduce((sum, fn) => fn(sum), arg)
const rr = pipe(toUpper, log('1st'), addExclamation, log('2nd'), double)(text)
console.log(rr, 'rr')
函数式与面向对象对比
比如我们要做一个太阳系出来,函数式编程更像是定好初始状态和物理定律,剩下的就不用操心了,因为初始状态会根据物理定律来进行改变,知道前一秒的状态,后一秒也是完全可预测的。
面向对象更像是抽象出一个个类,比如我有恒星类,行星类,小行星类。然后定义恒星有什么状态,有什么方法,行星有什么状态,有什么方法。然后 new 一个恒星出来,new 几个行星出来,让他们调用自己或者别人的方法,来改变自身的状态。每个行星自己的状态都是不可预测的,比如你不知道下一秒别人会不会碰撞你(即你不知道别人的状态,而别人的状态会影响到你的状态)。
![8b78cae4112c82bbb6935ffde0095c04.png](https://img-blog.csdnimg.cn/img_convert/8b78cae4112c82bbb6935ffde0095c04.png)
总而言之,面向对象编程更适合有良好解耦的多模块系统,每个模块维护自己的数据与方法,只对外暴露接口。函数式编程更适合需要从整体上对整个程序进行控制的系统。
Ref
- https://egghead.io/courses/just-enough-functional-programming-in-javascript