Skip to content
Article
Authors
On this page
Published on

JavaScript 函数式编程

Article
Authors

函数式编程思想是一种对运算过程的抽象,这里的函数是指数学意义上映射关系(如y=sin(x)),也称为纯函数,而编程语言定义的函数除了运算,往往带有副作用,如 IO 操作、影响外部变量等。

1. 函数式编程基础

1.1 函数是一等公民

函数只是一个特殊的对象,可以用变量表示(函数是一等公民)

js
function add(x, y) {
  return x + y
}

const sum = add
sum(1, 2) // 3

1.2 高阶函数

  • 函数可以作为参数
  • 函数可以作为返回值

函数作为参数,模拟实现 forEach 函数:

js
function myForEach(array, f) {
  for (let i = 0; i < array.length; i++) {
    f(array[i])
  }
}

myForEach([1, 2, 3], (itemÏ) => {
  console.log(item)
})

函数作为返回值,封装一个 once 函数,生成一个只能执行一次的函数:

js
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 等,以下为模拟实现:

js
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

闭包的本质:函数在执行时会放到一个执行栈上,函数执行完从栈中移除。但是当作用域成员被外部引用则不能释放,因此内部函数依然可以访问外部函数成员。

js
// 示例一
function makefn() {
  let msg = "hello"
  return function () {
    console.log(msg)
  }
}

const fn = makefn()
fn() // hello

2. 函数式编程核心

2.1 纯函数

纯函数即数学意义上的函数,表示的是一种映射关系,相同输入永远得到相同输出,没有副作用。

纯函数示例:

js
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 是一个纯函数库,封装了对常用数据类型的操作方法,有的副作用,有的没有。

js
const _ = require("lodash")

const l = [1, 2, 3, 4, 5, "A", "b"]

_.first(l)
_.last(l)
_.reverse(l) // 有副作用

纯函数的好处:

  • 可缓存
  • 可测试
  • 方便并行处理
js
// 缓存纯函数的值,可以避免重复计算,提高性能

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 副作用

纯函数根据相同的输入会永远的到相同的输出,如果函数依赖于外部状态就无法保证相同输出,即带来副作用:

js
let threshold = 18

// 不是纯函数,依赖外部状态,如果外部变量改变,相同输入可能会得到不同输出
const ifAdult = (age) => age > threshold

// 纯函数,不依赖外部,相同输入始终到相同输出
const ifAdult2 = (age) => {
  let threshold = 18
  return age > threshold
}

2.3 柯里化


先看示例:

js
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 实现的的通用柯里化方法:

js
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 方法:

js
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”形式

js
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 模块提供已经柯里化的方法,可以直接组合,方便函数式编程:

js
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(" "))
js
const logger = _.curry((tag, value) => {
  console.log(tag)
  return value
})

const f = compose(f1, logger("f1执行后:"), f2, logger("f2执行后:"), f3)

参考文档

  1. Understanding JavaScript currying - LogRocket Blog