JS 代码技巧 vol.8 --- 20 个函数式编程实战,把 if/else 拍扁的骚操作
小不的代码技巧系列第 8 期 主题:函数式编程 这次盘 20 个------主打"反例 vs 函数式"对比,每个 tip 都附一段"翻车名场面"代码,看着乐呵看着学 😎 20 个技巧一次打包,分心法、参数、组合、数组、避坑五组。
哈喽哇!我是小不 ,不简说的不~
作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂
今天这期,主打一个 函数式编程------不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱
Ps:文末有惊喜(不是广告!)
🚨 翻车名场面 3 连(先看乐子再学技术)
以下代码均来自真实项目,作者已跑路,bug 已遗留
名场面 1:100 行 if-else 金字塔
js
function calc(type) {
if (type === 'a') return 1;
else if (type === 'b') return 2;
else if (type === 'c') return 3;
// ...
else if (type === 'z') return 26;
else return '未知';
}
// 后来加新类型?Ctrl+C/V 吧(叹气)
名场面 2:传说中的"万能函数"
js
function doSomething(data, flag, type, mode, isAdmin, isVip, isNew) {
// 200 行,没人敢动
if (flag && type === 'a' && mode === 1) {
if (isAdmin) {
if (!isVip) {
// ...
}
} else {
// ...
}
}
// ...
}
名场面 3:链式调用到第 5 层就找不到入参了
js
const result = R.pipe(
R.filter(x => x.active),
R.map(x => x.items),
R.flatten,
R.map(transform),
R.groupBy(x => x.category),
)(data);
// 出 bug 调试:'我传进去的 data 呢?' -- 同事已疯
以上都是真实踩过的------学完这期你就能少写 30% 这种代码。
一、心法(4 个)
1. 纯函数:相同输入相同输出
反例:
js
// ❌ 依赖外部状态,谁调谁知道
let tax = 0.1;
function calcPrice(price) {
return price * (1 + tax);
}
calcPrice(100); // 110
tax = 0.2;
calcPrice(100); // 120 ← 同样的输入,不同的输出
正解:
js
// ✅ 纯函数:所有依赖都从参数进
function calcPrice(price, tax) {
return price * (1 + tax);
}
calcPrice(100, 0.1); // 110
calcPrice(100, 0.1); // 110 ← 稳定
坑在哪 :纯函数容易测试 (不用 mock 全局),容易缓存 (memoize),容易并发 (无副作用)。写不纯的函数时心里要有数------这是埋雷。
2. 不可变数据:原对象纹丝不动
反例:
js
// ❌ 改原数据,隔壁模块一脸懵
function addItem(cart, item) {
cart.items.push(item); // 直接改原数组
return cart;
}
const myCart = { items: [] };
addItem(myCart, { id: 1 });
// React/Vue 里:状态改了视图没更新,定位半天
正解:
js
// ✅ 返回新对象
function addItem(cart, item) {
return {
...cart,
items: [...cart.items, item],
};
}
const myCart = { items: [] };
const newCart = addItem(myCart, { id: 1 });
// myCart.items 还是 [] ← 原对象未动
坑在哪 :深拷贝要 structuredClone(见 part 02),别自己写递归 。性能敏感场景用 Immer(draft 模式改完自动 immutable)。
3. 高阶函数:函数当参数 / 返回值
反例:
js
// ❌ 每个数组操作写一遍循环
const doubled = [];
for (const x of arr) doubled.push(x * 2);
const filtered = [];
for (const x of arr) if (x > 0) filtered.push(x);
正解:
js
// ✅ map / filter / reduce 都是高阶函数
const doubled = arr.map(x => x * 2);
const filtered = arr.filter(x => x > 0);
坑在哪 :高阶函数 = 接受函数作参数 / 返回函数。理解了这个,柯里化、组合、thunk 都是它的延伸。
4. 声明式 vs 命令式
反例(命令式:告诉电脑"怎么做"):
js
// ❌ 命令式
const result = [];
for (const x of arr) {
if (x > 0) result.push(x * 2);
}
正解(声明式:告诉电脑"要什么"):
js
// ✅ 声明式
const result = arr.filter(x => x > 0).map(x => x * 2);
坑在哪 :声明式代码更短、更易读 ,但性能不一定好 (链式调用会创建中间数组)。热路径用命令式,冷路径用声明式。
二、参数处理(4 个)
5. 柯里化:多参变单参链
反例:
js
// ❌ 每次都得传全参数
function add(a, b, c) { return a + b + c; }
add(1, 2, 3); // 6
// 想做个"加 1 的"?自己写新函数
const addOne = (b, c) => add(1, b, c);
正解:
js
// ✅ 柯里化:每次只传一个
const add = a => b => c => a + b + c;
add(1)(2)(3); // 6
add(1)(2); // b => c => 1 + b + c(部分应用)
坑在哪 :手写柯里化麻烦,用 lodash 的 _.curry:
js
const add = _.curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6 ← 灵活,可以一次性传多个
6. 偏函数:预设部分参数
反例:
js
// ❌ 每次都写 `method: 'POST'`
fetch(url, { method: 'POST', body: data });
fetch(url2, { method: 'POST', body: data2 });
fetch(url3, { method: 'POST', body: data3 });
正解:
js
// ✅ 用偏函数"固化"通用参数
const post = (url, body) => fetch(url, { method: 'POST', body });
post('/api/a', data);
post('/api/b', data2);
// 顺手把 URL 也固化
const postA = url => body => fetch(url, { method: 'POST', body });
坑在哪 :偏函数和柯里化容易混淆 ------柯里化是"每次一个参数",偏函数是"预设部分参数"。实际用 lodash 的 _.partial 就行。
7. flip:参数顺序互换
反例:
js
// ❌ 内置函数参数顺序不对
const sub = (a, b) => a - b;
[1, 2, 3].map(x => sub(10, x)); // [9, 8, 7] -- 但意图不明显
正解:
js
// ✅ flip 互换参数顺序
const flip = fn => (a, b) => fn(b, a);
const flippedSub = flip((a, b) => a - b);
flippedSub(10, 3); // -7 ← 现在是 b - a
// 实战:filter 的 callback 是 (item, index),但你想用 (index, item)
['a', 'b', 'c'].filter(flip((i, item) => i % 2 === 0));
坑在哪 :flip 在回调签名不匹配 时救命------特别是 (value, key) vs (key, value) 这种场景。
8. 占位参数:部分应用
反例:
js
// ❌ 只想固定"中间"参数,没现成方案
function greet(greeting, name, punctuation) {
return `${greeting}, ${name}${punctuation}`;
}
greet('Hi', '小不', '!');
greet('Hi', '老王', '?');
// 想固定 greeting='Hi' 但不固定 name / punctuation?
正解:
js
// ✅ lodash 占位符 `_.placeholder`
const { placeholder } = _;
const sayHi = _.partial(greet, 'Hi', placeholder, placeholder);
sayHi('小不', '!'); // "Hi, 小不!"
sayHi('老王', '?'); // "Hi, 老王?"
// Ramda 写法更优雅
const sayHi = R.partial(greet, 'Hi');
sayHi('小不', '!'); // 同样工作
坑在哪 :没占位符时偏函数只能固定最左边的参数 。需要固定中间/后面参数时老老实实用 _.placeholder。
三、函数组合(4 个)
9. pipe:左到右串函数
反例:
js
// ❌ 嵌套地狱,从里往外读
const result = format(
filter(active, map(price, addTax(cart)))
);
正解:
js
// ✅ pipe:从左到右读
const { result } = pipe(
addTax,
map(price),
filter(active),
format,
)(cart);
// 一眼看到数据流:加税 → 提价 → 过滤 → 格式化
坑在哪 :手写 pipe 也就 10 行,lodash 是 _.flow,Ramda 是 R.pipe------选一个用熟。
js
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const fn = pipe(addTax, mapPrice, filterActive);
fn(cart);
10. compose:右到左(数学风格)
反例:
js
// ❌ 数学家写函数组合是这个方向:f(g(x))
const result = filter(active, map(price, addTax(cart)));
// 实际想用:addTax → map → filter
正解:
js
// ✅ compose 顺序自然从右往左
const result = compose(
filter(active),
map(price),
addTax,
)(cart);
// 也对,但读起来反人类(个人偏好 pipe)
坑在哪 :compose 和 pipe 顺序相反 ------新团队用 pipe 居多,可读性更好。别两个混用,同一项目统一。
11. tap:链中副作用不打断链
反例:
js
// ❌ 想在链中 console.log 调试,得拆开
const result1 = addTax(cart);
console.log('after tax:', result1);
const result2 = map(price, result1);
console.log('after map:', result2);
const result3 = filter(active, result2);
正解:
js
// ✅ tap:传入值,返回原值
const tap = fn => x => (fn(x), x);
pipe(
addTax,
tap(x => console.log('after tax:', x)),
map(price),
tap(x => console.log('after map:', x)),
filter(active),
)(cart);
坑在哪 :tap 还能用来埋点上报 ------函数式风格做 AOP,不改业务逻辑。
12. identity:啥都不做
反例:
js
// ❌ map 只想拿原数组,没招
const items = [1, 2, 3].map(x => x);
正解:
js
// ✅ identity:占位神器
const identity = x => x;
[1, 2, 3].map(identity);
// pipe 里某个步骤临时不需要处理?插个 identity
pipe(
addTax,
identity, // 暂时跳过
filter(active),
)(cart);
坑在哪 :identity 在条件分支里超有用------"有时做变换有时不做",统一接口。
js
const transform = needTransform ? double : identity;
[1, 2, 3].map(transform);
四、数组高阶(4 个)
13. map / filter / reduce 链
反例:
js
// ❌ 三个循环 + 中间变量
let result = [];
for (const x of arr) {
if (x.active) {
const y = x.value * 2;
if (y > 10) result.push(y);
}
}
正解:
js
// ✅ 链式一眼读完
const result = arr
.filter(x => x.active)
.map(x => x.value * 2)
.filter(y => y > 10);
坑在哪 :链长超过 5 个就该考虑抽函数。reduce 不是用来代替 map/filter 的------能分清就分清。
14. flatMap:map + flatten 一步
反例:
js
// ❌ map 后再 flat
arr.map(x => x.tags).flat();
// [].concat(...arr.map(x => x.tags)); // 老写法
正解:
js
// ✅ flatMap 一步
arr.flatMap(x => x.tags);
// 还能过滤:返回空数组就是过滤
arr.flatMap(x => x.active ? [x.value] : []);
坑在哪 :flatMap 只展平一层 。要深展平用 flat(Infinity)。比 map().flat().flat() 干净太多。
15. memoize:记忆化缓存
反例:
js
// ❌ 同一个输入算 N 遍
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
fib(40); // 1 秒+
正解:
js
// ✅ 加缓存
const memoize = fn => {
const cache = new Map();
return (x) => {
if (!cache.has(x)) cache.set(x, fn(x));
return cache.get(x);
};
};
const fastFib = memoize(fib);
fastFib(40); // 几十毫秒
坑在哪 :缓存默认用全等比较 (Map 拿第一个参数当 key),多参数函数要换 key 策略。lodash.memoize 还支持自定义 resolver。
16. once:只跑一次
反例:
js
// ❌ 初始化逻辑担心被多次调用
let initialized = false;
function init() {
if (initialized) return;
// 初始化代码
initialized = true;
}
正解:
js
// ✅ once 一行搞定
const once = fn => {
let called = false;
return (...args) => {
if (called) return;
called = true;
return fn(...args);
};
};
const init = once(() => {
// 初始化代码
});
坑在哪 :once + memoize + debounce + throttle 这四件套写工具函数必背 。Lodash 都有现成实现:_.once。
五、避坑(4 个)
17. reduce 滥用:能 map 干就别 reduce
反例:
js
// ❌ 用 reduce 做 map 的事
[1, 2, 3].reduce((acc, x) => [...acc, x * 2], []);
// [2, 4, 6]
正解:
js
// ✅ map 一行
[1, 2, 3].map(x => x * 2);
坑在哪 :reduce 是累积器 ------适合"转对象、分组、扁平化"。map / filter / flatMap 能干的别用 reduce,可读性差一大截。
18. 链式调试:tap + 命名
反例:
js
// ❌ 链式出 bug 不知道哪步出问题
const result = arr
.map(...)
.filter(...)
.map(...)
.filter(...)
.map(...);
正解:
js
// ✅ 链中插 tap 调试
const result = arr
.map(...)
.tap(x => console.log('after map1:', x))
.filter(...)
.tap(x => console.log('after filter:', x))
.map(...);
// 或者把每步抽成命名函数
const step1 = x => x.map(...);
const step2 = x => x.filter(...);
const step3 = x => x.map(...);
pipe(step1, tap(console.log), step2, tap(console.log), step3)(arr);
坑在哪 :链长超过 5 个就该拆函数 。命名后的函数比链式可读 。别把链式当炫技。
19. lodash/fp:函数式工具库
反例:
js
// ❌ 业务里手写 5 个工具函数
const myFilter = (arr, fn) => arr.filter(fn);
const myMap = (arr, fn) => arr.map(fn);
const myPipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
// 重复造轮子
正解:
js
// ✅ lodash/fp 或 Ramda
import { pipe, filter, map, prop } from 'lodash/fp';
const getActiveUserNames = pipe(
filter(prop('active')),
map(prop('name')),
);
getActiveUserNames(users);
// 自带柯里化 + 数据后置(point-free)
坑在哪 :lodash 普通版是数据优先 (_.map(arr, fn)),lodash/fp 是函数优先 (fp.map(fn)(arr))------FP 项目一定装 fp 版。
20. 业务场景应用:什么时候该上 FP
反例:
js
// ❌ 业务里硬上 FP,反而难维护
const calcOrderPrice = pipe(
when(isVip, applyDiscount(0.8)),
when(isHoliday, applyDiscount(0.95)),
applyTax(0.1),
formatCNY,
);
// 新人看不懂,不敢改
正解:
js
// ✅ 业务里"数据流"清晰的场景才用
// 适合:表单 pipeline、状态机、数据转换链、过滤搜索
const processOrder = pipe(
validateItems, // 校验
applyDiscounts, // 优惠
calcTax, // 税
formatOutput, // 格式化
);
// 不适合:单步操作、复杂分支、含异步副作用重的
坑在哪 :FP 不是银弹 ------能拍扁 if-else,但异步、副作用、状态管理 还是得用命令式或专门的库。业务里"局部用 FP"比"全 FP"务实。
📦 收个尾
这 20 个技巧浓缩一下:
- 最高频踩坑 :reduce 滥用、链式超过 5 个不拆、
{...obj}浅拷贝导致状态改不动 - 最值得收藏:pipe + tap 调试、lodash/fp 工具库、memoize 缓存
- 核心思路 :纯函数 + 不可变 + 组合 = FP 三件套,别在业务里硬上,局部用更香
学到了就是赚到了,犹豫徘徊等于白来~
写到最后
想要啥技巧?评论区甩个题目过来~
- 你刚踩的坑
- 项目里反复写的代码
- 想搞清楚但一直懒得查的 API
小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~
Ps:三连随缘,催更的会被打 😂