Skip to main content
我最早以为函数式就是多用 map,后来被共享状态坑过,才开始在意纯函数和不可变数据。

Q1: 函数式编程的历史?为什么前端需要它?

1

1930 年: Lambda 演算的数学基础

数学家 Alonzo Church 提出了 Lambda 演算——一种用函数来表示一切计算的数学系统。它的核心思想惊人地简单:一切皆函数,函数可以作为参数传递,也可以作为返回值。 这成为了一切函数式编程语言的理论基础。
2

1958 年: Lisp 语言诞生

John McCarthy 基于 Lambda 演算设计了 Lisp——世界上第二种高级编程语言。Lisp 首次将”函数作为一等公民”引入编程语言,函数可以像数字和字符串一样被传递和操作。
3

1995 年: JavaScript 天然支持函数式

Brendan Eich 设计 JavaScript 时深受 Scheme(Lisp 方言)影响。函数是一等公民、闭包、高阶函数——这些函数式特性从一开始就是 JavaScript 的核心能力,只是大多数开发者把它当”面向对象语言”用了二十年。
函数式编程解决的核心问题:
函数式编程的核心不是”用函数写代码”,而是”消除副作用让代码可预测”。

🚫 没有函数式思维

你开了一家餐厅,厨师每次做菜都往一个公共调料锅里加调料(共享可变状态)。第一个厨师加了盐,第二个厨师又加了盐——最后汤咸得没法喝。没人知道是谁加多了,因为大家都在改同一锅汤。

✅ 引入函数式思维

每个厨师拿到自己的食材(输入),独立做出一份菜(输出),不碰别人的锅。每份菜的味道只取决于食材和做法,和”厨房里还有谁在做菜”无关。这就是纯函数——输出只取决于输入,没有副作用。
面试时不要上来就讲 Monad、Functor 这些高级概念。先讲清楚”纯函数”和”不可变数据”这两个核心,然后用 map/filter/reduce 展示你的日常编码习惯,最后如果被追问再深入柯里化、组合、函子等高级话题。

Q2: 纯函数 —— 函数式编程的基石

纯函数是函数式编程最基本的概念:相同的输入,永远得到相同的输出,且没有任何可观察的副作用。

常见的副作用(Side Effects)

一个函数没有 return 值不代表它是不纯的,一个函数有 return 值也不代表它是纯的。关键看两点:
  1. 相同输入是否总是产生相同输出
  2. 是否有可观察的副作用

纯函数的巨大优势

纯函数就像数学中的 f(x) = x + 1。f(1) 永远等于 2,不管你调用多少次、在什么时间调用、调用的顺序是什么。它不依赖”今天是星期几”,也不修改”x 的值”。这种确定性让代码变得极度可预测——你不需要”运行一遍才知道结果”,看一眼函数签名就够了。

Q3: 不可变数据 —— 永远不要修改,而是创建新的

不可变数据的核心思想是:数据一旦创建,就不能被修改。如果需要”修改”,实际上是创建一个新版本的副本。

不可变数据的常用操作

展开运算符 ... 只做浅拷贝。如果对象有嵌套属性,内层仍然是引用:
对于复杂的嵌套结构,推荐使用 Immer 库——它用 Proxy 实现了”看起来像在修改,实际上创建新副本”的效果。

Immer 的实现原理

不可变数据就像 Git 的版本管理。你不会直接修改 main 分支上的代码——你创建一个新分支、做修改、然后合并。原来的 main 分支始终不变,任何时候你都可以回溯到任意版本。不可变数据也是这样:每次”修改”都创建一个新版本,旧版本永远不变。

Q4: 高阶函数 —— 函数作为一等公民

高阶函数是满足以下任一条件的函数:接收函数作为参数,或者返回函数作为结果。 它是函数式编程的核心工具。

最常见的三个高阶函数:map、filter、reduce

reduce 的强大之处

reduce 不只是”求和”——它可以实现 map 和 filter 的所有功能:

函数作为一等公民的实际应用

高阶函数让”行为”可以被传递、组合和复用——这是函数式编程区别于面向对象的核心能力。

Q5: 柯里化与函数组合 —— 函数的乐高积木

柯里化(Currying)

柯里化把一个接收多个参数的函数,转换成一系列只接收一个参数的函数:

通用柯里化函数

函数组合(Composition)

函数组合把多个函数串联成一个函数,数据从右到左流过每个函数:

pipe:从左到右的组合

  • compose(h, g, f):从右到左执行,数学上的 f(g(h(x))) 约定
  • pipe(f, g, h):从左到右执行,更符合代码的阅读顺序
Redux 使用 compose,Ramda 使用 compose(从右到左),Unix 管道符 | 使用 pipe(从左到右)。

Q6: 函子(Functor)—— 可以被 map 的容器

函子是函数式编程中”容器”的概念:一个实现了 map 方法的容器,map 让你可以对容器内的值进行变换,而不需要取出值。

Maybe 函子:优雅地处理 null

Either 函子:优雅地处理错误

函子就像一个安全快递箱。你可以通过 map 在箱子外面贴标签、写地址(变换内容),但不需要打开箱子。Maybe 函子是一个”防碎箱”——如果箱子里是空的,你贴标签的操作会被自动跳过,不会报错。Either 函子是一个”有退款保障的箱子”——如果快递出了问题(Left),后续的操作全部跳过,但错误信息被保留下来了。

Q7: 函数式编程在 React 中的应用

React 的设计理念深度根植于函数式编程。理解这一点,才能理解 Hooks 为什么这样设计。

UI = f(state)

Hooks 的函数式本质

不可变数据在 React 中的重要性

React Hooks 的设计哲学就是函数式编程:纯函数组件、不可变状态、副作用隔离。

Q8: 函数式编程的常见误区

误区一:函数式 = 不用变量

误区二:函数式 = 性能差

误区三:函数式 = 不需要面向对象

不是所有代码都适合函数式:
  • I/O 操作(网络请求、文件读写)天然是有副作用的,不可能变成纯函数
  • DOM 操作是有副作用的,React 用 useEffect 把它隔离到”副作用区域”
  • 性能敏感的热路径(如游戏渲染循环)可能需要命令式优化
函数式编程的正确用法是:用纯函数处理业务逻辑,把副作用推到系统的边缘。

底层源码:函数式特性在引擎中的实现

V8 的 TurboFan 编译器会利用纯函数的特性进行激进优化:
Redux 的 combineReducers 是函数组合的经典应用——每个 reducer 是一个纯函数,组合后仍然是纯函数:

十、及格回答 vs 满分回答

问题:“什么是函数式编程?你在项目中怎么用的?”

及格回答(60-70分):
“函数式编程就是用函数来写代码,避免修改变量。用 map、filter、reduce 处理数组,React Hooks 也是函数式的。”
问题出在:知道概念但没有深度。 满分回答(90-100分):
函数式编程的核心不是”用函数写代码”,而是消除副作用、让代码可预测。它的两个基石是纯函数和不可变数据。纯函数保证相同输入永远得到相同输出,不依赖外部状态,这让代码极度可测试——不需要 mock 任何东西,直接断言输入输出。[1]
在 React 项目中,我大量使用函数式思想:组件是纯函数(state → UI),状态更新用不可变数据(展开运算符创建新对象),复杂状态逻辑用 useReducer(纯函数 reducer)。Redux 的 reducer 就是纯函数——接收旧 state 和 action,返回新 state,不修改原 state。[2]
我还会用函数组合来构建数据处理管道。比如处理 API 响应数据时,用 pipe 把”解析 → 过滤 → 转换 → 格式化”串联起来,每个步骤都是纯函数,可以独立测试和复用。柯里化让我可以预设部分参数,创建特化的工具函数。[3]
但函数式不是银弹。I/O 操作天然是有副作用的,我把它们推到系统边缘(React 的 useEffect、Redux 的 middleware),让核心业务逻辑保持纯粹。实际项目中是函数式和命令式的混合——纯函数处理业务逻辑,命令式处理 I/O 和性能敏感路径。

常见追问

答题要点: 关键看”是否依赖或修改外部状态”。高分回答: “判断一个函数是否纯函数,只看两点:
  1. 相同输入是否总是产生相同输出?(排除 Math.random()、Date.now() 等)
  2. 是否有可观察的副作用?(修改外部变量、DOM 操作、I/O 操作)
实际项目中,大多数函数都不是纯函数——因为业务代码必然涉及 API 调用和 DOM 更新。关键是把纯函数和非纯函数分离开:业务逻辑用纯函数写(计算价格、过滤数据、格式化文本),副作用推到边界(useEffect 处理 API、事件处理器处理 DOM)。
答题要点: Hooks 让函数组件拥有了状态和副作用能力,同时保持了函数式的声明式风格。高分回答: “React Hooks 的设计哲学就是函数式编程:
  1. useState:不可变状态更新——不是修改 state,而是用新值替换旧值
  2. useEffect:把副作用隔离到’效果区’——渲染是纯的,副作用是受控的
  3. useReducer:Redux 风格的纯函数状态管理——reducer 是纯函数
  4. useMemo/useCallback:缓存纯函数的计算结果——就是 memoize 的思想
class 组件的 this.state 是可变的——this.setState 直接修改组件实例。Hooks 的 useState 返回全新的值,不修改任何东西。这就是从面向对象到函数式的转变。”
答题要点: 柯里化的核心价值是”参数复用”和”函数特化”。高分回答: “柯里化在实际项目中最常见的用途是’预设参数,创建特化函数’:
React 中的 useCallback 也经常配合柯里化:
答题要点: compose 从右到左,pipe 从左到右,选择取决于阅读习惯。高分回答: “compose 和 pipe 的唯一区别是执行方向:
  • compose(f, g, h) = x => f(g(h(x))),从右到左,数学约定
  • pipe(h, g, f) = x => f(g(h(x))),从左到右,更直观
实际选择:
  • Redux 用 compose(因为函数式编程社区的传统)
  • RxJS 用 pipe(因为数据流从左到右更直观)
  • Unix 管道 | 用 pipe(cat file | grep error | sort)
我个人倾向 pipe,因为它和代码的执行顺序一致,更容易阅读。”
答题要点: 浅拷贝的开销很小,深层嵌套才是问题。高分回答: “不可变数据的性能影响主要在两方面:
  1. 对象创建开销:每次更新都创建新对象。但现代 JS 引擎(V8)的对象创建非常快,小对象的创建开销可以忽略
  2. 浅拷贝开销:{ ...obj } 只拷贝一层,对于扁平对象几乎无感
真正的性能问题在深层嵌套:
优化方案:
  1. 扁平化状态结构:Redux 推荐 normalized state,避免深层嵌套
  2. Immer:用 Proxy 实现’看起来像修改,实际创建新副本’,只在变化的路径上创建新对象
  3. Immutable.js:持久化数据结构,结构共享——未变化的部分复用旧引用”
答题要点: middleware 是函数组合的经典应用——柯里化 + compose。高分回答: “Redux 的 middleware 是柯里化和函数组合的完美结合:
applyMiddleware 用 compose 把多个 middleware 串联起来,形成一个增强版的 dispatch 函数。每个 middleware 可以:
  • 在 next() 前做处理(日志、校验)
  • 决定是否调用 next()(拦截)
  • 在 next() 后做处理(日志、副作用)
  • 替换 action(异步中间件 thunk)
这就是函数式编程中的’装饰器模式’——不修改原始 dispatch,通过组合添加新功能。”
答题要点: 闭包是函数式编程的核心工具,但也是内存泄漏的常见原因。高分回答: “函数式编程本身不会导致内存泄漏,但闭包(函数式编程的核心工具)如果不注意,会持有不必要的引用:
React 中常见的闭包陷阱:
答题要点: pipe + 纯函数 + 错误处理。高分回答: “在实际项目中,我经常用函数组合来构建数据处理管道。以一个电商订单处理为例:
每个函数都是纯函数,可以独立测试。新增一个’税费计算’步骤?加一个函数就行。这种可插拔、可测试的管道就是函数式编程在工程中的价值。“

延伸阅读与引用参考