☰
JavaScript函数式编程入门:纯函数、柯里化与组合实战
2026/10/26 14:24:18 网站建设 项目流程

很多同学在项目里写 JavaScript 一段时间后,会发现代码越来越难维护。尤其是在处理数组数据、异步逻辑和状态变更时,for 循环嵌套、临时变量散落、函数之间互相修改数据,导致一个很小的改动就能引发连锁问题。这个阶段,学习函数式编程是一个非常合适的突破口。

函数式编程不是一门新语言,也不是某种“高端玩法”。它是一套关于“如何组织代码、如何管理数据变化”的思路。JavaScript 天生支持函数式编程,数组的 map、filter、reduce 就是最常见的函数式方法。这篇文章会从函数式编程的核心概念讲起,然后结合大量可运行的 JavaScript 示例,带你掌握纯函数、不可变性、高阶函数、柯里化、函数组合等实用方法。

如果你是刚接触函数式编程的前端或 Node.js 开发者,这篇文章可以帮你建立一套清晰的心智模型。如果你已经在项目中写过一些 map、filter,那么重点可以放在函数组合和工程化实践部分。文中所有代码都可以直接复制到浏览器控制台或 Node.js 环境运行。

1. 函数式编程到底是什么

1.1 从命令式到声明式

先看一段最常见的 JavaScript 代码:

const numbers = [1, 2, 3, 4, 5, 6]; // 命令式写法:一步一步告诉计算机“怎么做” const result = []; for (let i = 0; i < numbers.length; i++) { if (numbers[i] % 2 === 0) { result.push(numbers[i] * 10); } } console.log(result); // [20, 40, 60]

这段代码的逻辑并不复杂:找出偶数,然后乘以 10。但阅读时需要逐行追踪变量 i 的变化、result 数组的 push 过程,心智负担比较重。

如果换成函数式写法:

const numbers = [1, 2, 3, 4, 5, 6]; const result = numbers .filter(n => n % 2 === 0) .map(n => n * 10); console.log(result); // [20, 40, 60]

这段代码表达的其实是“做什么”:先过滤出偶数,再映射为乘以 10 的结果。没有循环变量,没有中间状态,也没有手动 push。这就是声明式编程的思想,而函数式编程是声明式编程的一种典型代表。

1.2 函数式编程解决什么问题

JavaScript 项目随着规模增长,最常遇到以下问题:

  • 多个函数共享同一个对象或数组,某个函数不小心修改了数据,导致其他模块报错。
  • 代码中大量使用 for 循环,逻辑膨胀后难以测试。
  • 函数之间依赖外部状态,同样的输入可能得到不同输出,调试困难。
  • 业务逻辑分散在事件回调和异步函数中,代码执行顺序不直观。

函数式编程的核心理念就是“用纯函数组合出复杂逻辑,避免共享状态和副作用”。它并不排斥 JavaScript 的面向对象特性,而是在数据变换和业务逻辑层提供一套更可控的写法。

1.3 函数式编程在 JavaScript 中的定位

JavaScript 是多范式语言:既支持面向对象,也支持函数式,还支持事件驱动。这意味着你不需要“抛弃”之前的经验,只需要在合适的场景使用函数式思想。比如 React 的函数组件、Redux 的 reducer、Lodash 的链式调用、RxJS 的流式处理,底层都是函数式编程思想。

学习 JavaScript 函数式编程,并不需要先学 Haskell 这类纯函数式语言。JavaScript 的弱类型特性和动态对象系统,反而让函数式方法更容易上手。

2. 环境准备与版本说明

本文代码属于 JavaScript 原生语法,不需要安装额外框架。你可以选择以下任一方式运行示例:

运行环境使用方式
浏览器开发者工具打开 Chrome / Edge,按 F12,切到 Console(控制台)粘贴代码运行
Node.js新建一个.js文件,在终端执行node 文件名.js
在线代码平台比如 CodePen、StackBlitz、PlayCode,直接运行 JavaScript 片段

推荐使用 Node.js 环境,因为可以配合console.log查看输出,也方便后续练习组合函数和模块化写法。

# 查看 Node.js 版本 node -v # 运行 js 文件 node demo.js

如果你使用的是较新版本的 Node.js(16.x 及以上),本文所有语法都可以直接支持。部分代码会用到Array.prototype.flatMap、Array.prototype.reduce等内置方法,这些都是 ES2019 之前就标准化的 API,兼容性良好。

版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示思路。如果你在旧版本浏览器中运行,可能需要对箭头函数等 ES6 语法进行转译。

3. 核心概念拆解

3.1 纯函数:确定性是第一原则

函数式编程最基础的概念是纯函数。一个纯函数必须满足两个条件:

  1. 相同的输入永远返回相同的输出。
  2. 函数执行过程中不产生副作用(不修改外部变量、不打印、不发起请求、不操作 DOM)。

看一个对比:

// 非纯函数:依赖外部变量 discount let discount = 0.8; function getPriceWithDiscount(price) { return price * discount; // 外部变量一旦变化,同一输入结果不同 } // 非纯函数:修改外部状态 let total = 0; function addToTotal(price) { total += price; // 副作用:修改了 total return total; } // 纯函数 function calcPrice(price, discount) { return price * discount; } console.log(calcPrice(100, 0.8)); // 80 console.log(calcPrice(100, 0.8)); // 80,无论调用多少次结果都稳定

纯函数最大的优势是可以被安全地缓存、测试和并行执行。你不需要关心调用顺序,也不用担心某个模块改动了公共状态。

在 React 中,reducer 就被要求是纯函数:

// reducer 示例:不能直接修改 state function reducer(state = [], action) { switch (action.type) { case 'ADD_ITEM': return [...state, action.payload]; // 返回新数组 default: return state; } }

3.2 不可变性:数据不直接改,而是生成新数据

不可变性是指数据一旦创建,就不应该被直接修改。需要变更时,复制出一个新数据再操作。

JavaScript 的对象和数组都是引用类型,直接修改很容易影响其他变量:

const user = { name: '张三', age: 25 }; const copyUser = user; copyUser.age = 26; console.log(user.age); // 26,原对象被改了

函数式编程中,我们使用展开运算符或 Object.assign 创建新对象:

const user = { name: '张三', age: 25 }; const updatedUser = { ...user, age: 26 }; console.log(user.age); // 25,原对象不变 console.log(updatedUser.age); // 26

数组的不可变操作同样重要:

const list = [1, 2, 3]; // 新增元素:不 push,而是返回一个新数组 const added = [...list, 4]; // 删除元素:不 splice,而是 filter const removed = list.filter(n => n !== 2); // 修改元素:不直接赋值,而是 map const updated = list.map(n => n === 3 ? 30 : n); console.log(list); // [1, 2, 3] 原数组不变 console.log(added); // [1, 2, 3, 4] console.log(removed); // [1, 3] console.log(updated); // [1, 2, 30]

这里需要注意的是,展开运算符只能做到浅拷贝。如果数组里嵌套了对象,修改内层对象的属性仍然会影响原数据。更复杂的深拷贝,可以使用structuredClone或者 JSON 序列化,但要留意性能和函数丢失问题。

3.3 高阶函数:函数也是普通值

JavaScript 中函数是一等公民,可以像字符串、数字一样作为参数传递,也可以作为返回值返回。高阶函数就是“操作函数”的函数,它至少满足以下一种情况:

  • 接收函数作为参数。
  • 返回一个新函数。

数组的map、filter、reduce、forEach都是典型的高阶函数。你自己写的高阶函数通常用于抽象流程:

function withLog(fn) { return function (...args) { console.log('调用参数:', args); const result = fn(...args); console.log('返回结果:', result); return result; }; } function add(a, b) { return a + b; } const addWithLog = withLog(add); addWithLog(2, 3); // 调用参数: [2, 3] // 返回结果: 5

高阶函数让代码具备更强的复用性。公共逻辑可以抽到外层,具体差异可以通过参数传入。

3.4 柯里化与部分应用

柯里化是指将一个多参数函数转换成一系列单参数函数的过程。

// 普通多参数函数 function multiply(a, b) { return a * b; } // 柯里化版本 function curriedMultiply(a) { return function (b) { return a * b; }; } const multiplyBy2 = curriedMultiply(2); console.log(multiplyBy2(5)); // 10 console.log(multiplyBy2(8)); // 16

使用箭头函数可以写得更简洁:

const curriedMultiply = a => b => a * b; const multiplyBy3 = curriedMultiply(3); console.log(multiplyBy3(7)); // 21

柯里化的价值在于“预设公共参数”。比如接口请求的 baseURL、日志的前缀、配置项中的环境标识,都可以通过柯里化固定下来。

部分应用和柯里化类似,但不严格要求一次只传一个参数:

const partial = (fn, ...presetArgs) => (...laterArgs) => fn(...presetArgs, ...laterArgs); function request(method, url, data) { return `[${method}] ${url} data=${JSON.stringify(data)}`; } const getRequest = partial(request, 'GET'); const postRequest = partial(request, 'POST'); console.log(getRequest('/api/user', { id: 1 })); // [GET] /api/user data={"id":1}

4. 实用方法实战

4.1 数组核心方法:map / filter / reduce

这三个方法是最常用的函数式数组工具。

map:一对一映射

map返回一个新数组,每个元素都经过回调函数处理。

const users = [ { name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }, { name: 'Carol', age: 35 } ]; const names = users.map(user => user.name); console.log(names); // ['Alice', 'Bob', 'Carol'] // map 的完整回调参数:当前元素、索引、原数组 const nameWithIndex = users.map((user, index) => `${index + 1}. ${user.name}`); console.log(nameWithIndex); // ['1. Alice', '2. Bob', '3. Carol']
filter:按条件筛选

filter返回一个新数组,只包含回调返回 true 的元素。

const numbers = [10, 25, 36, 41, 58, 63]; const greaterThan30 = numbers.filter(n => n > 30); console.log(greaterThan30); // [36, 41, 58, 63] const adults = users.filter(user => user.age >= 30); console.log(adults); // [{ name: 'Bob', age: 30 }, { name: 'Carol', age: 35 }]
reduce:把数组收敛为单个值

reduce是整个数组方法中功能最强大的一个,它可以把数组“归约”成一个值:数字、对象、字符串,甚至另一个数组。

const orders = [ { product: '键盘', price: 299 }, { product: '鼠标', price: 129 }, { product: '显示器', price: 1499 } ]; // 计算总价 const totalPrice = orders.reduce((sum, order) => sum + order.price, 0); console.log(totalPrice); // 1927 // 按价格区间分类 const grouped = orders.reduce((acc, order) => { if (order.price < 300) { acc.low.push(order); } else { acc.high.push(order); } return acc; }, { low: [], high: [] }); console.log(grouped); // { // low: [{ product: '键盘', price: 299 }, { product: '鼠标', price: 129 }], // high: [{ product: '显示器', price: 1499 }] // }

reduce的第二个参数是初始值。初始值类型决定了返回结果类型,这是初学者比较容易忽略的点。如果省略初始值,reduce会使用数组第一个元素作为初始 acc,从第二个元素开始遍历,在空数组上会直接报错。

4.2 组合与管道:把函数串起来

函数组合是函数式编程的“组装工艺”。可以把多个单功能函数组合成一个新函数,数据从左边流到右边,或者从上向下流动。

先看一个不使用组合的版本:

const str = ' Hello JavaScript '; // 需求:去掉空格、转大写、拆成单词 const trimmed = str.trim(); const upper = trimmed.toUpperCase(); const words = upper.split(' '); console.log(words); // ['HELLO', 'JAVASCRIPT']

这个过程是把 str 当成一个参数,依次经过三个函数处理。我们可以封装一个pipe函数(管道),把数据自动传递给一串函数:

const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input); const trim = str => str.trim(); const toUpper = str => str.toUpperCase(); const splitBySpace = str => str.split(' '); const processStr = pipe(trim, toUpper, splitBySpace); console.log(processStr(' Hello JavaScript ')); // ['HELLO', 'JAVASCRIPT']

pipe的执行顺序是从左到右:先 trim,再 toUpper,最后 split。如果希望顺序反过来,可以用compose:

const compose = (...fns) => (input) => fns.reduceRight((acc, fn) => fn(acc), input); // compose 的调用顺序从右到左 const processStr2 = compose(splitBySpace, toUpper, trim); console.log(processStr2(' Hello JavaScript ')); // ['HELLO', 'JAVASCRIPT']

在实际项目中,pipe更符合阅读习惯,因为它和数据的流动方向相同。很多库也提供了类似实现,比如 Lodash 的_.flow就是从左到右的管道函数。

4.3 闭包与记忆化

闭包是函数式编程中一个基础且重要的概念。简单说,闭包就是“函数 + 它定义时所在的作用域”。当一个内部函数引用了外部函数的变量,即使外部函数已经执行完毕,内部函数仍然可以访问这些变量。

function createCounter() { let count = 0; return function increment() { count += 1; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3

闭包可以用于数据封装,也可以用于记忆化(memoization)。记忆化是一种缓存策略:如果函数接收相同的参数,直接返回之前计算过的结果,避免重复计算。

function memoize(fn) { const cache = new Map(); return function (...args) { const key = JSON.stringify(args); if (cache.has(key)) { console.log('从缓存命中:', key); return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; }; } // 一个比较耗时的计算函数 function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } const memoizedFib = memoize(fib); console.log(memoizedFib(40)); // 第一次明显比较慢 console.log(memoizedFib(40)); // 第二次命中缓存,很快

记忆化适合纯函数。如果函数有副作用,缓存结果可能和真实状态不一致,因此使用时需要确认函数确实满足“相同输入返回相同输出”。

4.4 灵活运用 flatMap 处理嵌套结构

实际业务中,经常遇到数组嵌套数组的情况。flatMap等价于先map再flat(1),适合扁平化和映射同时完成的场景。

const departments = [ { name: '前端组', members: ['张三', '李四'] }, { name: '后端组', members: ['王五', '赵六'] } ]; const allMembers = departments.flatMap(dept => dept.members); console.log(allMembers); // ['张三', '李四', '王五', '赵六']

另一个例子:根据商品生成多条订单记录。

const cart = [ { item: '笔记本', quantity: 2, price: 15 }, { item: '钢笔', quantity: 3, price: 8 } ]; const orderRows = cart.flatMap(product => Array.from({ length: product.quantity }, () => ({ item: product.item, price: product.price })) ); console.log(orderRows); // [ // { item: '笔记本', price: 15 }, // { item: '笔记本', price: 15 }, // { item: '钢笔', price: 8 }, // { item: '钢笔', price: 8 }, // { item: '钢笔', price: 8 } // ]

4.5 函数式处理异步数据

JavaScript 的异步逻辑也很适合用函数式思路组织。Promise本身就可以看作容器,then方法类似于map,可以把纯函数应用到异步结果上。

const fetchUser = (id) => Promise.resolve({ id, name: 'User' + id }); const fetchUserOrders = (user) => Promise.resolve([{ orderId: 1001, user: user.name }]); fetchUser(1) .then(user => { console.log('用户信息:', user); return user; }) .then(fetchUserOrders) .then(orders => { console.log('订单列表:', orders); });

更函数式的写法,可以把数据处理拆成多个单函数,然后用管道串起来:

const getUserId = id => ({ id }); const addName = user => ({ ...user, name: 'User' + user.id }); const addOrders = async (user) => { const orders = [{ orderId: 1001, user: user.name }]; return { ...user, orders }; }; const pipeAsync = (...fns) => (input) => fns.reduce((acc, fn) => acc.then(fn), Promise.resolve(input)); pipeAsync(getUserId, addName, addOrders)(2).then(console.log); // { id: 2, name: 'User2', orders: [{ orderId: 1001, user: 'User2' }] }

5. 完整实战:用函数式思路重构购物车结算

下面通过一个实际业务场景,把前面讲到的概念串起来。假设我们有一个购物车,包含商品名称、单价、数量、是否为折扣商品等字段,现在需要完成以下功能:

  1. 筛选出有效商品(数量大于 0)。
  2. 给折扣商品打 8 折。
  3. 计算出每个商品的小计金额。
  4. 计算购物车总价。
  5. 返回一组可读的结算明细。

5.1 定义数据源

const cart = [ { id: 1, name: '机械键盘', price: 399, quantity: 1, isDiscount: true }, { id: 2, name: '无线鼠标', price: 129, quantity: 0, isDiscount: false }, { id: 3, name: '显示器', price: 1499, quantity: 2, isDiscount: false }, { id: 4, name: 'USB-C 扩展坞', price: 259, quantity: 3, isDiscount: true } ];

5.2 拆分成纯函数

const isValidItem = item => item.quantity > 0; const applyDiscount = item => item.isDiscount ? { ...item, finalPrice: Number((item.price * 0.8).toFixed(2)) } : { ...item, finalPrice: item.price }; const calcSubtotal = item => ({ ...item, subtotal: Number((item.finalPrice * item.quantity).toFixed(2)) }); const calcTotal = items => items.reduce((sum, item) => sum + item.subtotal, 0); const formatSummary = items => ({ items: items.map(item => ({ name: item.name, finalPrice: item.finalPrice, quantity: item.quantity, subtotal: item.subtotal })), total: Number(calcTotal(items).toFixed(2)) });

5.3 使用 pipe 组合流程

const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input); const checkout = pipe( items => items.filter(isValidItem), items => items.map(applyDiscount), items => items.map(calcSubtotal), formatSummary ); const result = checkout(cart); console.log(JSON.stringify(result, null, 2));

输出如下:

{ "items": [ { "name": "机械键盘", "finalPrice": 319.2, "quantity": 1, "subtotal": 319.2 }, { "name": "显示器", "finalPrice": 1499, "quantity": 2, "subtotal": 2998 }, { "name": "USB-C 扩展坞", "finalPrice": 207.2, "quantity": 3, "subtotal": 621.6 } ], "total": 3938.8 }

可以看到,无效商品“无线鼠标”被过滤掉了,折扣商品的价格被重新计算,最终返回的是一个全新对象,原 cart 数据没有被修改。

如果把每个函数定义和 pipe 调用写在一个文件里,完整代码如下:

// checkout.js const cart = [ { id: 1, name: '机械键盘', price: 399, quantity: 1, isDiscount: true }, { id: 2, name: '无线鼠标', price: 129, quantity: 0, isDiscount: false }, { id: 3, name: '显示器', price: 1499, quantity: 2, isDiscount: false }, { id: 4, name: 'USB-C 扩展坞', price: 259, quantity: 3, isDiscount: true } ]; const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input); const isValidItem = item => item.quantity > 0; const applyDiscount = item => item.isDiscount ? { ...item, finalPrice: Number((item.price * 0.8).toFixed(2)) } : { ...item, finalPrice: item.price }; const calcSubtotal = item => ({ ...item, subtotal: Number((item.finalPrice * item.quantity).toFixed(2)) }); const calcTotal = items => items.reduce((sum, item) => sum + item.subtotal, 0); const formatSummary = items => ({ items: items.map(item => ({ name: item.name, finalPrice: item.finalPrice, quantity: item.quantity, subtotal: item.subtotal })), total: Number(calcTotal(items).toFixed(2)) }); const checkout = pipe( items => items.filter(isValidItem), items => items.map(applyDiscount), items => items.map(calcSubtotal), formatSummary ); console.log(JSON.stringify(checkout(cart), null, 2));
node checkout.js

5.4 代码可测试性分析

这种写法最大的好处是每个小函数都可以独立测试:

console.log(applyDiscount({ price: 100, isDiscount: true })); // { price: 100, isDiscount: true, finalPrice: 80 } console.log(calcSubtotal({ price: 100, quantity: 3, finalPrice: 80 })); // { price: 100, quantity: 3, finalPrice: 80, subtotal: 240 } console.log(isValidItem({ quantity: 0 })); // false console.log(isValidItem({ quantity: 2 })); // true

函数之间没有隐藏依赖,没有共享的可变状态,测试时不需要 mock 复杂的外部环境。

6. 常见问题与排查思路

函数式编程在 JavaScript 中使用时,会有一些典型的坑。这里整理一份高频问题清单。

问题现象常见原因解决思路
原数组或对象被意外修改直接使用 push、splice、属性赋值,破坏了不可变性改用展开运算符、map、filter、reduce 返回新数据
reduce 结果不是预期类型忘记传初始值,或者初始值类型不对检查 reduce 第二个参数,确定初始值类型与最终返回类型一致
柯里化后函数不执行把curriedAdd(1)写成了curriedAdd(1)但忘记调用返回的函数curriedAdd(1)(2);检查是否返回了函数而不是值
pipe/compose 的执行结果顺序不对混淆了从左到右和从右到左的执行方向统一使用 pipe 并坚持从左到右顺序,减少歧义
深拷贝后函数丢失使用 JSON.parse(JSON.stringify()) 处理包含方法的对象换用 structuredClone 或手动递归拷贝
this 指向错误高阶函数内部使用了普通 function,this 指向调用者使用箭头函数保持定义时上下文,或 bind 显式绑定
记忆化导致数据不刷新对非纯函数做缓存保证传入 memoize 的函数是纯函数,或加入失效机制

再展开几个常见错误场景。

6.1 reduce 空数组报错

const empty = []; const total = empty.reduce((acc, n) => acc + n); console.log(total); // TypeError: Reduce of empty array with no initial value

这里的原因是没有提供初始值,reduce 无法从空数组中取得首个 acc。解决方案很简单:

const total = empty.reduce((acc, n) => acc + n, 0); console.log(total); // 0

6.2 展开运算符的浅拷贝问题

const cart = [ { name: 'A', info: { price: 10 } } ]; const newCart = [...cart]; newCart[0].info.price = 999; console.log(cart[0].info.price); // 999,原数据被改了

由于info是嵌套对象,展开运算符只复制了外层数组,内部对象仍然是同一引用。如果业务上确实需要深拷贝,可以使用structuredClone:

const deepCopiedCart = structuredClone(cart); deepCopiedCart[0].info.price = 0; console.log(cart[0].info.price); // 10

6.3 高阶函数中的 this 丢失

const obj = { name: 'CSDN', tags: ['前端', '后端'], getTags() { return this.tags.map(function (tag) { return `${this.name}: ${tag}`; }); } }; console.log(obj.getTags()); // TypeError: Cannot read properties of undefined (reading 'name')

普通 function 在 map 回调中不继承外层方法中的 this。改成箭头函数即可:

const obj = { name: 'CSDN', tags: ['前端', '后端'], getTags() { return this.tags.map(tag => `${this.name}: ${tag}`); } }; console.log(obj.getTags()); // ['CSDN: 前端', 'CSDN: 后端']

排查这类问题,可以遵循以下清单:

  1. 报错信息是否指向某个回调函数内部?
  2. 回调使用的是普通 function 还是箭头函数?
  3. 是否误用了外部的可变对象?
  4. reduce 是否提供了符合预期类型的初始值?
  5. 数据在多个管道函数中流转时,每一步返回的类型是否发生了变化?

7. 最佳实践与工程建议

7.1 从过滤和映射开始,不急着引入复杂工具

很多团队引入函数式编程时容易一步到位过度设计,反而增加了理解成本。建议先只在数据变换场景使用map、filter、reduce,让团队成员熟悉这种返回新数据而非修改旧数据的写法。之后再逐步引入柯里化、函数组合等抽象。

7.2 保持函数短小,专注于单一职责

一个函数最好只做一件事。如果发现函数名需要多个动词才能描述,比如processAndFilterAndFormatData,大概率需要拆分了。函数式编程天然鼓励小函数组合,这也是代码可读性的来源之一。

7.3 命名要体现“变换”

处理数据时,优先使用动词或表示结果的变量名:

// 不推荐 const data = list.map(item => transform(item)); // 推荐 const formattedUsers = users.map(formatUser); const activeProducts = products.filter(isActive); const totalAmount = orders.reduce(sumPrice, 0);

好的命名能让管道代码变成一段“可读的流程描述”。

7.4 定义纯函数边界,隔离副作用

纯函数适合放业务计算逻辑,但真实项目不可能完全没有副作用:写日志、发请求、操作 localStorage 都算副作用。经验做法是:尽量把副作用推到程序“最外层”,内部业务逻辑保持纯函数。比如异步请求拿到数据后,先调用一组纯函数处理数据,最后再把结果用于渲染或持久化。

// 不推荐:副作用散落在各个小函数里 function process() { const data = getData(); const filtered = data.filter(item => { console.log('filter item', item); // 副作用 return item.active; }); saveToStorage(filtered); // 副作用 } // 更推荐:纯函数计算与副作用隔离 const filterActive = items => items.filter(item => item.active); const convertForStorage = items => items.map(item => ({ id: item.id })); function process() { const data = getData(); const active = filterActive(data); const readyToSave = convertForStorage(active); saveToStorage(readyToSave); }

7.5 避免大量工具函数堆叠导致过度抽象

当项目里出现大量只调用一次、没有复用价值的小函数时,函数式风格反而会降低可读性。函数式编程的目的是让代码清晰,而不是为了“看起来函数式”而把所有逻辑都拆成函数。一般建议:同一个处理方法在三个以上位置复用,才值得单独抽取。

7.6 用 TypeScript 增强函数式代码的可维护性

如果项目允许,配合 TypeScript 使用函数式编程体验会更好。函数签名可以明确参数和返回值,避免管道中数据类型错乱的问题。

type CartItem = { id: number; name: string; price: number; quantity: number; isDiscount: boolean; finalPrice?: number; subtotal?: number; }; const applyDiscount = (item: CartItem): CartItem => item.isDiscount ? { ...item, finalPrice: Number((item.price * 0.8).toFixed(2)) } : { ...item, finalPrice: item.price };

7.7 性能考虑

函数式风格创建了大量中间对象,这是它的一个代价。在普通业务场景中,这种代价基本可以忽略。如果处理超大数组或高性能计算场景,需要注意:

  • 避免在循环中重复创建大型对象。
  • 大数组多次 map 会产生多份中间数组,可以考虑用迭代器或 Generator 替代。
  • 对高频调用的纯函数使用记忆化。
  • 不要盲目深拷贝大型对象,尽量使用浅拷贝或持久化数据结构。

7.8 善用现有工具库

JavaScript 生态中已经有非常成熟的函数式工具库。如果不想自己维护pipe、compose等工具函数,可以引入 Lodash 或 Ramda。Lodash 适合渐进式使用,Ramda 更偏纯粹的函数式风格。在引入之前要评估包体积和维护成本。

8. 总结与学习路线

这篇文章从“为什么需要函数式编程”开始,介绍了纯函数、不可变性、高阶函数、柯里化、函数组合、记忆化等核心概念,并通过 map、filter、reduce、flatMap、pipe 等实用方法,演示了如何用函数式思路处理数组和异步数据。最后的购物车结算实战,完整展示了如何把业务逻辑拆分成多个纯函数,再用管道组装成可测试的业务流程。

到这一步,你应该已经能看懂并写出基础的函数式 JavaScript 代码了。下一步可以按以下方向继续深入:

  • 学习 Ramda 库的常用 API,了解真实项目中的函数式工具集。
  • 阅读 Redux Toolkit 源码,理解 reducer 中不可变更新的实现思路。
  • 学习 RxJS,掌握响应式编程与函数式编程结合的方式。
  • 尝试把现有项目中的一段命令式代码改造成函数式风格,并写单元测试验证。

实际项目中,建议优先关注两件事:第一,把数据变换相关的代码改造成纯函数,这一步收益最大;第二,严格控制副作用边界,让业务逻辑更容易测试。函数式编程不是银弹,但当代码规模变大、协作人数变多时,它提供的约束和确定性会显著降低维护成本。

如果你在改造过程中遇到问题,欢迎对照文章里的常见问题清单逐一排查。函数式编程是一套需要刻意练习的思维方式,多写、多重构、多读源码,慢慢就能形成直觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询