函数式编程思想是一种对运算过程的抽象,这里的函数是指数学意义上映射关系(如y=sin(x)),也称为纯函数,而编程语言定义的函数除了运算,往往带有副作用,如 IO 操作、影响外部变量等。
1. 函数式编程基础
1.1 函数是一等公民
函数只是一个特殊的对象,可以用变量表示(函数是一等公民)
function add(x, y) {
return x + y
}
const sum = add
sum(1, 2) // 3
1.2 高阶函数
- 函数可以作为参数
- 函数可以作为返回值
函数作为参数,模拟实现 forEach 函数:
function myForEach(array, f) {
for (let i = 0; i < array.length; i++) {
f(array[i])
}
}
myForEach([1, 2, 3], (itemÏ) => {
console.log(item)
})
函数作为返回值,封装一个 once 函数,生成一个只能执行一次的函数:
function once(fn) {
let done = false
return (...args) => {
if (!done) {
done = true
return fn(...args)
}
}
}
const pay = once(function (money) {
console.log(`消费了${money}元`)
})
pay(5)
pay(5)
pay(5)
// ---
消费了5元
数组的方法中有很多高阶函数,如 map、reduce、filter、every、some 等,以下为模拟实现:
const map = (arr, fn) => {
let temp = []
for (let i of arr) {
temp.push(fn(i))
}
return temp
}
const every = (arr, f) => {
let result = true
for (let i of arr) {
result = f(i)
if (!result) {
break
}
}
return result
}
💡 高阶函数的意义:把函数进一步封装抽象、屏蔽细节,只关注结果
1.3 闭包
闭包:通常,闭包是指使用一个特殊的属性 [[Environment]]
来记录函数自身的创建时的环境的函数。它具体指向了函数创建时的词法环境。
INFO
闭包的本质:函数在执行时会放到一个执行栈上,函数执行完从栈中移除。但是当作用域成员被外部引用则不能释放,因此内部函数依然可以访问外部函数成员。
// 示例一
function makefn() {
let msg = "hello"
return function () {
console.log(msg)
}
}
const fn = makefn()
fn() // hello
2. 函数式编程核心
2.1 纯函数
纯函数即数学意义上的函数,表示的是一种映射关系,相同输入永远得到相同输出,没有副作用。
纯函数示例:
const list = [1, 2, 3]
// slice方法是纯函数,不会改变list的值,即没有副作用
const l1 = list.slice(1, 2)
console.log(list, l1) // [ 1, 2, 3 ] [ 2 ]
// splice方法不是纯函数,会原地改变原list的值,即产生了副作用
const l2 = list.splice(1, 1)
console.log(list, l2) // [ 1, 3 ] [ 2 ]
💡 lodash 是一个纯函数库,封装了对常用数据类型的操作方法,有的副作用,有的没有。
const _ = require("lodash")
const l = [1, 2, 3, 4, 5, "A", "b"]
_.first(l)
_.last(l)
_.reverse(l) // 有副作用
纯函数的好处:
- 可缓存
- 可测试
- 方便并行处理
// 缓存纯函数的值,可以避免重复计算,提高性能
const _ = require("lodash")
const add = (x, y) => {
console.log("==>>", x, y)
return x + y
}
const addWithMemory = _.memoize(add)
console.log(add(1, 2))
console.log(add(1, 2))
console.log(addWithMemory(1, 2))
console.log(addWithMemory(1, 2))
2.2 副作用
纯函数根据相同的输入会永远的到相同的输出,如果函数依赖于外部状态就无法保证相同输出,即带来副作用:
let threshold = 18
// 不是纯函数,依赖外部状态,如果外部变量改变,相同输入可能会得到不同输出
const ifAdult = (age) => age > threshold
// 纯函数,不依赖外部,相同输入始终到相同输出
const ifAdult2 = (age) => {
let threshold = 18
return age > threshold
}
2.3 柯里化
先看示例:
const _ = require("lodash")
console.log(Math.pow(2, 2))
console.log(Math.pow(2, 3))
console.log(Math.pow(3, 3))
// 手动柯里化
const getPower = (power) => (number) => Math.pow(number, power)
const power2 = getPower(2) // 得到一个专门求平方的函数
const power3 = getPower(3) // 得到一个专门求立方的函数
console.log(power2(2))
console.log(power2(3))
console.log(power3(3))
什么是柯里化?
柯里化是 lambda 演算中的一个概念,但不要被它吓到,它很容易实现。柯里化是一个函数,它一次接受一个参数并返回一个期待下一个参数的新函数。它是一种函数转换,将函数从 f(a, b, c) 的可调用函数转换为 f(a)(b)(c) 的可调用函数。
javascript 中的柯里化是什么?
柯里化只是意味着评估具有多个参数的函数,并将它们分解为具有单个参数的函数序列。柯里化是当一个函数不是一次接受所有参数,而是接受第一个参数并返回一个新函数,该函数接受第二个参数并又返回一个新函数,该函数接受第三个参数,依此类推,直到所有论证完毕。
为什么要使用柯里化?
- 避免一次又一次地传递相同的变量
- 将您功能划分为多个更细粒度的函数,这些函数可以处理某一项职责。使功能更加纯净,不易出错和产生副作用。
- 在函数式编程中用于创建高阶函数。
如何实现柯里化?
先看看 lodash 实现的的通用柯里化方法:
const _ = require("lodash")
const threeSum = (a, b, c) => a + b + c
const getSum = _.**curry**(threeSum)
console.log(getSum(1, 2, 3)) //6 ,实参与形参个数相同时直接返回结果
console.log(getSum(1)(2, 3)) //6 ,实参与形参个数不同时返回一个函数
console.log(getSum(1, 2)(3)) //6
模拟实现 lodash 的 curry 方法:
const curry = (f) => {
return (...args1) => {
if (args1.length === f.length) {
// 使用f.length可以获取原函数f的形参个数!
return f(...args1)
} else {
return (...args2) => f(...[...args1, ...args2])
}
}
}
// 进一步简化(没必要,可读性差)
const curry2 =
(f) =>
(...args1) =>
args1.length === f.length ? f(...args1) : (...args2) => f(...[...args1, ...args2])
2.4 函数组合
函数组合可以把细粒度的函数重新组合成一个新函数
示例:将”NEVER SAY NEVER”
类型的字符串转换为 “never-say-never”
形式
const s = "NEVER SAY NEVER"
const temp1 = _.split(s, " ")
const temp2 = _.map(l1, (i) => i.toLowerCase())
const result = _.join(l2, "-")
console.log(s, temp1, temp2, result)
// 1.直接嵌套调用
_.join(
_.map(_.split(s, " "), (i) => i.toLowerCase()),
"-"
)
// 2.使用lodash通用方法组合函数
const f1 = (tag) => (s) => _.split(s, tag)
const f2 = (fn) => (list) => _.map(list, fn)
const f3 = (tag) => (list) => _.join(list, tag)
const f = _.flow(
f1(" "),
f2((i) => i.toLowerCase()),
f3("-")
)
f(s)
// 模拟实现lodash中的flow组合函数
const compose = (...args) => {
return (value) => {
return args.reduce((preValue, currentFn) => currentFn(preValue), value)
}
}
console.log(compose(filterListNumber, getListSum)(l))
上述案例中,使用 lodash 提供的函数没有被柯里化,无法直接进行组合,需要手动柯里化封装才能组合使用。而 lodash/fp
模块提供已经柯里化的方法,可以直接组合,方便函数式编程:
const fp = require("lodash/fp")
const s = "NEVER SAY NEVER"
const f = fp.flow(
fp.split(" "),
fp.map((i) => i.toLowerCase()),
fp.join("-")
)
f(s)
// 更复杂的组合如下:
const fn = fp.flow(fp.join(". "), fp.map(fp.flow(fp.first, fp.toUpper)), fp.split(" "))
const logger = _.curry((tag, value) => {
console.log(tag)
return value
})
const f = compose(f1, logger("f1执行后:"), f2, logger("f2执行后:"), f3)
参考文档