JS 代码技巧 vol.8 — 20 个函数式编程实战,把 if/else 拍扁的骚操作

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)

坑在哪composepipe 顺序相反 ------新团队用 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:三连随缘,催更的会被打 😂

相关推荐
那些年丶ny5 小时前
颜色扩展库
前端·javascript·数据可视化
ihuyigui5 小时前
海外签收通知短信接口
android·java·开发语言·前端·数据库·后端
YWL5 小时前
OpenLayers + Vue 2 使用指南(01)
前端·javascript·vue.js
暖和_白开水5 小时前
数据分析agent (七):contextvars 模块上下文request_id
java·前端·数据分析
程序员黑豆5 小时前
鸿蒙应用开发:Flex 组件从入门到实战
前端·华为·harmonyos
大爱编程♡5 小时前
Vue3+TypeScript+element-plus的文章管理项目(配后端接口)-退出及文章管理页面
前端·javascript·typescript
先吃饱再说5 小时前
React 组件设计:从 Props 传递到组件封装
前端·react.js
独立开阀者_FwtCoder6 小时前
最近做了一个健身小程序:智形健身助手,健身的佬们来提点意见
前端·javascript·github
舒灿6 小时前
喵ing Tab 1.0.0 正式发布
前端·ai编程
葡萄城技术团队6 小时前
当表格数据来自外部系统:SpreadJS 如何让异步公式一键刷新
前端