技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

You Should Learn Functional Programming in 2018(函数式编程入门与现代 JavaScript 实践)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/60-You Should Learn Functional Programming in 2018
本文目录6 个章节
  1. 一、简短历史
  2. 二、函数式编程的核心概念
  3. 三、函数式编程的应用价值
  4. 四、不要把函数式编程绝对化
  5. 五、JavaScript 中可以继续学习的方向
  6. 六、总结

You Should Learn Functional Programming in 2018(函数式编程入门与现代 JavaScript 实践)

原文发表于 2018 年。本文保留原文从 Lambda 演算、Lisp、Elixir 到高阶函数和不可变状态的路线,同时补充 JavaScript 语境下的纯函数、副作用、组合、异步和并发边界。

**先说明一个容易被夸大的结论:**函数式编程不是“取代面向对象”的唯一答案,也不是禁止一切变量修改。它是一组帮助我们控制数据流和副作用的思想;现代 JavaScript 通常采用混合范式。

一、简短历史

函数式编程有很长的历史。Lisp 在 1958 年问世,它把列表、符号计算和函数作为一等概念;更早的 Lambda 演算则提供了研究“函数抽象”和“函数应用”的数学模型。Clojure、Scala、Erlang、Haskell、Elixir 等语言,分别以不同方式吸收了函数式思想。

Lambda 演算常用两个概念解释:

  • 函数抽象(function abstraction):给一个表达式引入参数和名称;
  • 函数应用(function application):将具体值传入函数并求值。

例如,下面的表达式表示一个把参数加一的函数:

f = λx. x + 1
f(5) => 5 + 1

这段表示法有助于理解函数式编程的理论来源,但 JavaScript 开发不需要直接使用 Lambda 演算。JavaScript 的函数、闭包和表达式已经提供了相似的实践工具。

NASA 7090 计算机房:早期计算机历史背景图

图片来源:Wikimedia Commons:NASA Computer Room 7090。图片来自美国国家档案与记录管理局,Wikimedia 页面标注了其版权/授权信息;重新发布时仍应遵守来源页面的许可要求。

二、函数式编程的核心概念

2.1 函数是一等值

函数是一等值,意味着函数可以像数字、字符串和对象一样:

  • 保存到变量;
  • 作为参数传给另一个函数;
  • 作为函数的返回值;
  • 放入数组或对象中。

Elixir 原文示例可以写成:

double = fn x -> x * 2 end
double.(2) # 4

JavaScript 中对应的写法是:

const double = value => value * 2

console.log(double(2)) // 4
console.log([1, 2, 3].map(double)) // [2, 4, 6]

函数表达式、箭头函数和普通函数都可以是一等值。箭头函数不会创建自己的 thisargumentsprototype,因此选择箭头函数还是普通函数应根据调用语义,而不是因为“函数式编程必须使用箭头函数”。

2.2 高阶函数

高阶函数(higher-order function)至少满足下面一个条件:接收函数作为参数,或返回一个函数。map 是典型例子:它把一个转换函数应用到集合的每个元素,并返回新数组。

const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(value => value * 2)

console.log(doubled) // [2, 4, 6, 8, 10]
console.log(numbers) // [1, 2, 3, 4, 5]

map 不会改变原数组,但回调函数仍然可能修改外部变量或元素对象。因此“使用高阶数组方法”不等于“代码自动纯净”。

const users = [{ name: 'A', active: true }, { name: 'B', active: false }]

// 转换数据,不修改 users
const names = users.filter(user => user.active).map(user => user.name)

// 虽然使用了 map,但仍然产生了副作用
const result = users.map(user => {
  user.seen = true
  return user
})

console.log(names) // ['A']
console.log(result === users) // false,但 result 中的对象仍与 users 共享

2.3 不可变性(immutable state)

在 Elixir 这样的语言中,数据结构本身通常不可变:更新操作会返回新值,而不会修改旧值。

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}

JavaScript 的对象和数组默认可变。我们可以通过约定、复制、Object.freeze() 或专门的不可变数据结构来降低意外修改:

const original = Object.freeze({
  name: 'hello',
  tags: Object.freeze(['js'])
})

// 严格模式下直接修改会抛错;非严格模式下可能静默失败
// original.name = 'world'

const updated = {
  ...original,
  name: 'world',
  tags: [...original.tags, 'fp']
}

console.log(original) // { name: 'hello', tags: ['js'] }
console.log(updated) // { name: 'world', tags: ['js', 'fp'] }

Object.freeze() 只进行浅冻结,嵌套对象仍需要递归冻结;它也不等同于深拷贝。工程中不必把所有对象都冻结,通常只在状态边界、开发检查或明确需要不可变语义的地方使用。

2.4 纯函数

纯函数在相同输入下总是得到相同输出,并且没有可观察的外部副作用:

function add(a, b) {
  return a + b
}

function formatUser(user) {
  return `${user.name} <${user.email}>`
}

下面的函数不是纯函数,因为它读取时间、随机数或修改外部变量:

const events = []

function record(value) {
  events.push(value)
  return value
}

function now() {
  return Date.now()
}

纯函数并不意味着“函数内部绝不能调用任何 API”。关键在于副作用是否被隔离、输入是否显式、结果是否可预测。例如可以把时间作为参数传入:

function isExpired(expiresAt, currentTime) {
  return currentTime >= expiresAt
}

console.log(isExpired(100, 120)) // true

实际应用通常采用“纯核心 + 副作用边界”:网络、DOM、日志、存储和随机数在边界处完成,核心数据转换尽量保持纯净。

2.5 副作用

副作用包括但不限于:

  • 修改外部变量、对象或数组;
  • 读写 DOM、文件、数据库、网络和 localStorage
  • 输出日志;
  • 修改随机数生成器、时间或全局配置;
  • 抛出异常或触发可观察的异步操作。

副作用不是错误。没有副作用,程序无法与世界交互;函数式编程的目标是让副作用显式、集中且容易测试,而不是把副作用伪装成不存在。

三、函数式编程的应用价值

原文提到云计算、可扩展性、并发和分布式系统,这些方向今天仍然重要,但不能把某个公司的规模表现简单归因于函数式编程。WhatsApp 使用 Erlang、Discord 使用 Elixir 的案例说明了语言运行时、消息模型、团队工程实践等因素的价值,不是“只要使用函数式语言就能获得同样结果”。原文中的 2015 年 WhatsApp 用户数和 Discord 文章统计也属于当时的历史数据,不应当作当前指标。相关历史资料可读:Wired 对 WhatsApp 的 2015 年报道Discord 的 GenStage 案例

函数式思想在现代 JavaScript 中常见于:

  • mapfilterreduce 表达数据转换;
  • 使用纯函数编写校验、格式化和状态更新;
  • 用不可变更新降低共享状态带来的耦合;
  • 用组合函数构建可复用的数据处理管道;
  • 将 I/O 和 UI 更新放在清晰的边界;
  • Promiseasync/await 描述异步数据流。

3.1 组合(composition)

组合是把小函数连接成更大的函数。下面的 pipe 从左到右执行:

const pipe = (...functions) => input =>
  functions.reduce((value, fn) => fn(value), input)

const trim = value => value.trim()
const parseNumber = value => Number(value)
const doubleNumber = value => value * 2

const parseAndDouble = pipe(trim, parseNumber, doubleNumber)
console.log(parseAndDouble(' 21 ')) // 42

要组合异步函数,需要明确 Promise 边界,不能直接把同步 reduce 当作异步管道:

const pipeAsync = (...functions) => input =>
  functions.reduce(
    (promise, fn) => promise.then(fn),
    Promise.resolve(input)
  )

const loadAndFormat = pipeAsync(
  async id => ({ id, name: 'Ada' }),
  async user => user.name.toUpperCase()
)

loadAndFormat(1).then(console.log) // ADA

3.2 闭包

闭包是函数与其词法环境的组合。它可以保存状态,但保存状态不代表状态一定不可变:

function createCounter() {
  let count = 0

  return {
    read: () => count,
    increment: () => ++count
  }
}

const counter = createCounter()
console.log(counter.read()) // 0
counter.increment()
console.log(counter.read()) // 1

闭包可以帮助隐藏实现细节,但也可能因为长期保存大对象、事件处理器或定时器而延长对象生命周期。需要在组件销毁时移除监听器,在不需要时清理定时器。

3.3 声明式和命令式

命令式代码描述“如何一步步做”:

const doubled = []
for (const value of [1, 2, 3]) {
  doubled.push(value * 2)
}

声明式代码更强调“结果是什么”:

const doubled = [1, 2, 3].map(value => value * 2)

声明式写法通常更易组合,但并不保证更快。对热点路径、复杂控制流和性能敏感代码,清晰的循环可能比层层 reduce 更合适;应以测量结果和可维护性为准。

四、不要把函数式编程绝对化

原文把面向对象编程描述为无法应对并发,这个结论过于绝对。面向对象、函数式、过程式和数据导向设计都可以用于大型系统;并发安全取决于共享状态、同步方式、消息模型和运行时,而不是仅由范式名称决定。

同样,以下说法也需要谨慎:

  • “不可变数据一定更快”:复制会产生成本,结构共享和持久化数据结构可以缓解,但仍需测量;
  • “纯函数解决所有并发问题”:I/O、资源竞争和取消仍需设计;
  • “链式数组方法一定优于循环”:多个中间数组可能增加分配,现代引擎也可能优化,但不能假设;
  • “函数式编程禁止修改”:更准确的说法是限制共享可变状态,把局部可变性控制在小范围内。

推荐的工程实践是:

  1. 让数据转换、校验和业务规则尽可能纯;
  2. 在模块边界明确输入、输出和副作用;
  3. 用不可变更新处理共享状态,用局部可变变量优化实现;
  4. 对异步操作显式处理错误、取消和并发数量;
  5. 用测试验证函数的输入、输出和边界,而不是只追求“函数式”的外观。

五、JavaScript 中可以继续学习的方向

六、总结

函数式编程的长期价值不在于把所有代码写成一串箭头函数,而在于帮助我们:

  • 把函数当作可组合的数据;
  • 降低隐式共享状态;
  • 隔离并管理副作用;
  • 用纯函数表达可测试的业务规则;
  • 在需要时选择不可变更新、组合和声明式数据处理。

2018 年的原文仍然适合作为函数式编程的入门引子;截至现在,更稳妥的实践是将这些思想与 JavaScript 的对象模型、异步模型、模块系统和实际性能需求结合起来。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS