阅读视图

发现新文章,点击刷新页面。

一个函数超过20行? 聊聊我的函数式代码洁癖

pankaj-patel-_SgRNwAVNKw-unsplash.jpg

大家好,我又来了😁

我得承认,我有个毛病,或者说洁癖吧。

在Code Review的时候,当我点开一个*.js / *.ts文件,看到一个函数洋洋洒洒地写了50行、80行,甚至更多时,我的第一反应不是去读它的逻辑,而是生理性地发慌😖。

我会下意识地在评论区留下一句:这个函数是不是太长了?能不能拆一下?

20行这个数字,是我给自己设的一个 代码量阈值。它不绝对,但足够灵敏。

我知道,很多人会觉得我这是小题大做、形式主义。但今天我想聊聊,这个洁癖背后,隐藏的是一个被函数式思想洗礼过的、关于代码可维护性、可测试性和认知成本的严肃思考。


为什么长函数让人如此发慌?😒

一个超过20行的函数,对我来说,通常意味着三场灾难:

1. 阅读成本极高

// 这是一个超过 50 行的函数
// 目的:根据用户数据生成报告并发送邮件(其实做了三件事)
function handleUserReport(users, sendEmail, isAdmin) {
  let result = [];
  let flag = false;

  console.log("开始处理用户数据...");

  for (let i = 0; i < users.length; i++) {
    let u = users[i];
    if (u.age > 18) {
      if (u.active) {
        if (u.score > 80) {
          result.push({ name: u.name, status: "优秀" });
          flag = true;
        } else if (u.score > 60) {
          result.push({ name: u.name, status: "良好" });
        } else {
          result.push({ name: u.name, status: "待提升" });
        }
      } else {
        if (isAdmin) {
          result.push({ name: u.name, status: "非活跃但保留" });
        } else {
          result.push({ name: u.name, status: "非活跃" });
        }
      }
    } else {
      if (u.active) {
        result.push({ name: u.name, status: "未成年用户" });
      }
    }
  }

  console.log("用户数据处理完毕");
  console.log("生成报告中...");

  let report = "用户报告:\n";
  for (let i = 0; i < result.length; i++) {
    report += `${result[i].name} - ${result[i].status}\n`;
  }

  if (flag) {
    console.log("存在优秀用户!");
  }

  if (sendEmail) {
    console.log("准备发送邮件...");
    // 模拟邮件发送逻辑
    for (let i = 0; i < result.length; i++) {
      if (result[i].status === "优秀") {
        console.log(`已发送邮件给:${result[i].name}`);
      }
    }
  }

  console.log("处理完成。");
  return report;
}

上面👆这个50多行的函数,就像一篇没有分段的短文。你必须从头到尾把它加载到你的大脑里,才能理解它到底在干嘛。

  • 第5行定义的一个flag变量,在第15行被修改了。

  • 中间夹杂着三层if/else嵌套。

  • 它到底做了几件事?天知道🤷‍♂️。

    这种函数,是可写,不可读的。写的人洋洋得意,几个月后他自己回来维护,一样骂娘😠。

2. 根本无法单元测试

我们来谈谈单元测试。你怎么去测试一个50行的、混合了数据请求、数据格式化和UI状态更新的函数?

先看代码👇:

// 一个50行的混合函数:既请求接口、又格式化数据、还更新UI状态
async function loadUserProfile(userId) {
  setLoading(true);

  try {
    // 1️⃣ 请求数据
    const response = await fetch(`/api/user/${userId}`);
    const data = await response.json();

    // 2️⃣ 本地缓存
    localStorage.setItem('lastUserId', userId);

    // 3️⃣ 格式化数据
    const displayName = data.firstName + ' ' + data.lastName;
    const ageText = data.age ? `${data.age}岁` : '未知年龄';

    // 4️⃣ UI状态更新
    setUser({
      name: displayName,
      age: ageText,
      hobbies: data.hobbies?.join('、') || '无'
    });

    // 5️⃣ 额外副作用
    if (data.isVIP) {
      trackEvent('vip_user_loaded');
      showVIPBadge();
    }

    setLoading(false);
  } catch (error) {
    console.error('加载失败', error);
    setError('加载用户信息失败');
    setLoading(false);
  }
}

测试代码:

// 测试代码(伪代码)
test('loadUserProfile should set formatted user data', async () => {
  // Mock 一堆外部依赖
  global.fetch = jest.fn().mockResolvedValue({
    json: () => Promise.resolve({ firstName: 'Tom', lastName: 'Lee', age: 28, isVIP: true })
  });
  localStorage.setItem = jest.fn();
  const setUser = jest.fn();
  const setLoading = jest.fn();
  const setError = jest.fn();
  const trackEvent = jest.fn();
  const showVIPBadge = jest.fn();

  // 还要通过依赖注入或hook替换上下文...
  await loadUserProfile(123);

  // 然后验证每一步是否被正确调用
  expect(fetch).toHaveBeenCalledWith('/api/user/123');
  expect(localStorage.setItem).toHaveBeenCalledWith('lastUserId', 123);
  expect(setUser).toHaveBeenCalledWith({
    name: 'Tom Lee',
    age: '28岁',
    hobbies: '无'
  });
  expect(trackEvent).toHaveBeenCalledWith('vip_user_loaded');
  expect(showVIPBadge).toHaveBeenCalled();
  expect(setLoading).toHaveBeenLastCalledWith(false);
});

你根本没法测试。你只能去集成测试。

为了测试它,你不得不mock掉fetch、localStorage、useState... 你会发现,你的测试代码,比你的业务代码还长、还复杂。

3. 你看不见的地雷

函数越长,它顺手去干点脏活的概率就越大。

举个例子👇:

// 名字看起来挺纯洁的 —— 获取用户配置
// 实际上它干了很多事没人知道...
function getUserConfig(userId) {
  console.log('开始获取用户配置...');

  // 1️⃣ 顺手改了全局变量
  globalCache.lastRequestTime = Date.now();

  try {
    // 2️⃣ 发起网络请求
    const res = fetch(`/api/config/${userId}`);
    const data = res.json();

    // 3️⃣ 顺手改了一下全局设置
    window.__APP_MODE__ = data.isAdmin ? 'admin' : 'user';

    // 4️⃣ 顺手写了一点 localStorage
    localStorage.setItem('lastConfigUser', userId);

    // 5️⃣ 格式化返回数据
    const config = {
      theme: data.theme || 'light',
      lang: data.lang || 'en-US'
    };

    return config;
  } catch (err) {
    console.error('获取配置出错', err);

    // 6️⃣ 顺手派发了一个事件
    window.dispatchEvent(new CustomEvent('config_load_failed', { detail: { userId } }));

    // 7️⃣ 顺手清空了一个全局标记
    globalCache.lastRequestTime = null;

    return { theme: 'light', lang: 'en-US' }; // 假装有个默认值
  }
}

调用者根本不知道它干了些什么 😵‍💫

const config = getUserConfig(42);
console.log(config.theme); // 看起来很正常

// 但此时:
// window.__APP_MODE__ 已被改动
// localStorage 里写入了 lastConfigUser
// globalCache.lastRequestTime 已变化
// 如果请求失败,还会触发一个全局事件

  • 它在函数的中间,顺手改了一个全局变量。

  • 它在catch块里,顺手dispatch了一个event

  • 它顺手往window上挂了个东西。

    这种充满隐形副作用的函数,是系统中最不可预测的地雷。你根本不知道你调用它,会影响到哪里。


谈一谈 函数式思想

我的洁癖,其实是来源于函数式编程思想。

我并不追求写出高阶组合子那些高深的东西。我只坚守两个最朴素的原则:

函数必须小,且只做一件事

这是 单一职责原则 的终极体现。一个函数,就只做一件事。

  • getUserData就只负责fetch

  • formatUserData就只负责格式化。

  • setUserState就只负责更新状态。

    一个函数超过20行,对我来说,往往就是它至少做了两件以上的事情的强烈信号。

追求纯函数,隔离掉它的一切副作用

一个纯函数:给它什么(入参),它就吐出什么(返回),绝不搞小动作。

我追求的目标,就是把所有的业务逻辑和计算,都抽成纯函数。而那些不得不做的脏活(比如API请求、DOM操作),则被我隔离在最外层的协调函数里。


重构一个函数

我们来看一个在React项目里,极其常见的函数(绝对超过20行):

// 场景:一个提交用户注册的函数
async function handleRegister(formData) {
  setLoading(true);
  
  // 1. 业务逻辑:验证
  if (!formData.username) {
    showToast('用户名不能为空');
    setLoading(false);
    return;
  }
  if (formData.password.length < 6) {
    showToast('密码不能少于6位');
    setLoading(false);
    return;
  }
  
  // 2. 业务逻辑:数据转换
  const apiPayload = {
    user: formData.username,
    pass: btoa(formData.password + 'my_salt'), // 假设的加密
    source: 'web',
    registerTime: new Date().toISOString(),
  };

  // 3. 副作用:API请求
  try {
    const result = await api.post('/register', apiPayload);
    
    // 4. 副作用:更新UI状态
    if (result.code === 200) {
      setUserData(result.data.user);
      trackEvent('register_success');
      showToast('注册成功!');
      router.push('/dashboard');
    } else {
      showToast(result.message);
    }
  } catch (err) {
    showToast(err.message);
    trackEvent('register_fail', { msg: err.message });
  } finally {
    setLoading(false);
  }
}

这个函数,就是一场灾难。它混合了4-5种职责,你根本没法测试它。

重构过程如下👇:

1.先分离纯业务逻辑(可测试)

// 纯函数1:验证逻辑 (可独立测试)
// (5行)
export function validateRegistration(formData) {
  if (!formData.username) return '用户名不能为空';
  if (formData.password.length < 6) return '密码不能少于6位';
  return null; // 验证通过
}

// 纯函数2:数据转换 (可独立测试)
// (7行)
export function createRegisterPayload(formData) {
  return {
    user: formData.username,
    pass: btoa(formData.password + 'my_salt'),
    source: 'web',
    registerTime: new Date().toISOString(),
  };
}

2.再分离它的副作用

// 副作用函数1:API调用
// (3行)
export async function postRegistration(payload) {
  return api.post('/register', payload);
}

// 副作用函数2:处理成功后的UI逻辑
// (6行)
function handleRegisterSuccess(userData) {
  setUserData(userData);
  trackEvent('register_success');
  showToast('注册成功!');
  router.push('/dashboard');
}

// 副作用函数3:处理失败后的UI逻辑
// (3行)
function handleRegisterFail(error) {
  showToast(error.message);
  trackEvent('register_fail', { msg: error.message });
}

3.最后重组函数

现在,我们原来的handleRegister函数,变成了一个清晰的调用者:

// (18行)
async function handleRegister(formData) {
  // 1. 验证
  const validationError = validateRegistration(formData);
  if (validationError) {
    showToast(validationError);
    return;
  }
  
  setLoading(true);
  try {
    // 2. 转换
    const payload = createRegisterPayload(formData);
    // 3. 执行
    const result = await postRegistration(payload);
    // 4. 响应
    if (result.code === 200) {
      handleRegisterSuccess(result.data.user);
    } else {
      handleRegisterFail(new Error(result.message));
    }
  } catch (err) {
    handleRegisterFail(err);
  } finally {
    setLoading(false);
  }
}

等等!你这个handleRegister函数,不还是快20行了吗?😂

是的,但你发现区别了吗?这个函数,几乎没有任何逻辑 ,它只负责调用其他小函数。它像一个流程图,清晰得一目了然。

而所有的业务逻辑(validatecreatePayload),都被我拆分到了可独立测试、可复用、可预测的纯函数里。这,就是这次的重构的价值。


20行代码的标准 不是一个KPI,它是一个预警

它在提醒我们,这个函数的 负载 可能已经超标了,它在 单一职责 的路上可能已经走偏了。

这种洁癖,不是为了追求代码的短小,而是为了追求代码的简单可预测

在一个由几十万行代码构成的、需要长期维护的系统里,简单和可预测,是比炫技(屎代码💩),要宝贵一百倍😁。

技术、业务、管理:一个30岁前端的十字路口

image.png

上个月,我刚过完30岁生日。

没有办派对,就和家人简单吃了顿饭。但在吹蜡烛的那个瞬间,我还是恍惚了一下。

30岁,对于一个干了8年的前端来说,到底意味着什么?

前几天,我在做团队下半年的规划,看着表格里的一个个名字,再看看镜子里的自己,一个问题在我脑子里变得无比清晰:

我职业生涯的下一站,到底在哪?

28岁之前

在28岁之前,我的人生是就行直线。

我的目标非常纯粹:成为一个技术大神。我的快乐,来自于搞懂一个Webpack的复杂配置、用一个巧妙的Hook解决了一个棘手的渲染问题、或者在Code Review里提出一个让同事拍案叫绝的优化。

这条路的升级路径也非常清晰:

初级(学框架) -> 中级(懂原理) -> 高级(能搞定复杂问题)

我在这条路上,跑得又快又开心。

30岁的十字路口

但到了30岁,我当上了技术组长,我发现,这条直线消失了。取而代之的,是一个迷雾重重的十字路口。

我发现,那些能让我晋升到高级的技能,好像并不能帮我晋升到下一个级别了。

摆在我面前的,是三条截然不同,却又相互纠缠的路。


技术路线——做技术专家

  • 这条路成为一个 主工程师 或 架构师。不带人,不背KPI,只解决公司最棘手的技术难题。比如,把我们项目的INP从200ms优化到100ms以下,或者主导设计公司下一代的跨端架构。

  • 这当然是我的舒适区。我爱代码,我享受这种状态。这条路,是我最熟悉、最擅长的。

  • 焦虑点:我真的能成为那个最顶尖的1%吗?前端技术迭代这么快,我能保证我5年后,还能比那些25岁的年轻人,学得更快、想得更深吗?当我不再是团队里最能打的那个人时,我的价值又是什么?


业务路线——更懂的产品工程师

  • 不再只关心怎么实现,而是去关心为什么要做?深入理解我们的商业模式、用户画像、数据指标。不再是一个接需求的资源,而是成为一个能和产品经理吵架、能反向推动产品形态的合作伙伴。

  • 我发现,在公司里,那些真正能影响决策、晋升最快的工程师,往往都是最懂业务的。他们能用数据和商业价值去证明自己工作的意义,而我,还在纠结一个技术实现的优劣。

  • 焦虑 :这意味着我要走出代码的舒适区,去开更多的会,去啃那些枯燥的业务文档,去和各种各样的人扯皮。我一个技术人,会不会慢慢变得油腻了?


管理——做前端Leader

  • 这就是我现在正在尝试的。我的工作,不再是写代码,而是让团队更好地写代码。我的KPI,不再是我交付了多少,而是我们团队交付了多少。

  • 老板常说的影响力杠杆。我一个人写代码,战斗力是1。我带一个5人团队,如果能让他们都发挥出1.2的战斗力,那我的杠杆就是6。这种成就感,和写出一个完美函数,是完全不同的。

  • 这是我最焦虑的地方:

    我上周二,开了7个会,一行代码都没写。

    晚上9点,我打开VS Code,看着那些我曾经最熟悉的代码库,突然有了一丝陌生感。我开始恐慌:我的手艺是不是要废了?如果有一天,我不当这个Leader了,我还能不能凭技术,在外面找到一份好工作?


这三个问题,在我脑子里盘旋了很久。我试图三选一,但越想越焦虑。

直到最近,我在复盘一个项目时,才突然想明白:

这根本不是一个三选一的十字路口。

这三条路,是一个优秀的技术人,在30岁之后,必须三位一体、同时去修炼的内功。

  • 一个不懂技术的Leader,无法服众,也做不出靠谱的架构决策。
  • 一个不懂业务的专家,他的技术再牛,也可能只是屠龙之技,无法为公司创造真正的价值。
  • 一个不懂管理(影响他人)的工程师,他的想法再好,也只能停留在自己的电脑上,无法变成团队的战斗力。

image.png

DOTA2的世界里,有一个英雄叫 祈求者(Invoker),他有冰、雷、火三个元素,通过不同的组合,能释放出10个截然不同的强大技能。

我觉得,30岁之后的前端,就应该成为一个祈求者。

我们不再是那个只需要猛点一个技能的码农。我们的挑战,在于如何在不同的场景下,把这三个元素,组合成最恰当的技能,去解决当下最复杂的问题。

这条路,很难,但也比25岁时,要有趣得多。

与所有在十字路口迷茫的同行者,共勉🙌。

重新思考CSS Reset:normalize.css vs reset.css vs remedy.css,在2025年该如何选?

what-is-css.png

我带团队Review一个新项目的启动代码时,有一个文件我一定会仔细看,那就是CSS Reset

它虽然不起眼,但却像我们整个CSS架构的地基。地基打不好,上面的楼盖得再漂亮,也容易出问题,后期维护成本会非常高。

从十多年前 reset.css 横空出世,到后来normalize.css 成为事实标准,再到近几年出现的一些新方案,CSS Reset的理念,其实也在不断演进。

但现在都2025年10月了,IE早已入土为安,主流浏览器对标准的支持也空前一致。我们还有必要像十年前那样做重置样式吗?

今天,我就想聊聊我对这几个主流方案的看法,以及在我们团队的当前项目中,我是如何选择的。


reset.css

  • 它的原理:非常暴力直接——抹平所有浏览器默认样式margin, padding, font-size, line-height...通通归零,h1pulli在外观上变得一模一样,所有元素都回到最原始、最裸的状态。

  • 代码片段感受一下

    /* http://meyerweb.com/eric/tools/css/reset/ 
       v2.0 | 20110126
       License: none (public domain)
    */
    
    html, body, div, span, applet, object, iframe,
    h1, h2, h3, h4, h5, h6, p, blockquote, pre,
    a, abbr, acronym, address, big, cite, code,
    del, dfn, em, img, ins, kbd, q, s, samp,
    small, strike, strong, sub, sup, tt, var,
    b, u, i, center,
    dl, dt, dd, ol, ul, li,
    fieldset, form, label, legend,
    table, caption, tbody, tfoot, thead, tr, th, td,
    article, aside, canvas, details, embed, 
    figure, figcaption, footer, header, hgroup, 
    menu, nav, output, ruby, section, summary,
    time, mark, audio, video {
            margin: 0;
            padding: 0;
            border: 0;
            font-size: 100%;
            font: inherit;
            vertical-align: baseline;
    }
    /* HTML5 display-role reset for older browsers */
    article, aside, details, figcaption, figure, 
    footer, header, hgroup, menu, nav, section {
            display: block;
    }
    body {
            line-height: 1;
    }
    ol, ul {
            list-style: none;
    }
    blockquote, q {
            quotes: none;
    }
    blockquote:before, blockquote:after,
    q:before, q:after {
            content: '';
            content: none;
    }
    table {
            border-collapse: collapse;
            border-spacing: 0;
    }
    
  • 优点:提供了一个绝对干净、可预测,非常适合那些需要从零开始、高度定制视觉风格的网站。

  • 2025年的缺点

    1. 太粗暴了:它移除了很多有用的默认样式。比如,你写了一个<ul>,却发现前面的项目符号没了,还得自己手动加回来。
    2. 破坏了语义化:一个<h1>在视觉上和<p>毫无区别,这在开发初期,会削弱HTML语义化的默认视觉反馈。
    3. 调试困难:当你在DevTools里审查一个元素时,你看到的样式,和它本该有的默认样式天差地别,这会增加调试的心智负担。

在2025年,对于绝大多数项目,我不推荐再使用这种粗暴的Reset样式。


normalize.css

screenshot-20251009-114007.png

  • 原理:与reset.css完全相反——保留有用的浏览器默认样式,只修复已知的浏览器不一致和Bug。它不在重置,而是修正。

  • 代码片段感受一下

    /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
    /* Document
       ========================================================================== */
    
    /**
     * 1. Correct the line height in all browsers.
     * 2. Prevent adjustments of font size after orientation changes in iOS.
     */
    
    html {
      line-height: 1.15; /* 1 */
      -webkit-text-size-adjust: 100%; /* 2 */
    }
    
    /* Sections
       ========================================================================== */
    
    /**
     * Remove the margin in all browsers.
     */
    
    body {
      margin: 0;
    }
    
    /**
     * Render the `main` element consistently in IE.
     */
    
    main {
      display: block;
    }
    
    /**
     * Correct the font size and margin on `h1` elements within `section` and
     * `article` contexts in Chrome, Firefox, and Safari.
     */
    
    h1 {
      font-size: 2em;
      margin: 0.67em 0;
    }
    
    /* Grouping content
       ========================================================================== */
    
    /**
     * 1. Add the correct box sizing in Firefox.
     * 2. Show the overflow in Edge and IE.
     */
    
    hr {
      box-sizing: content-box; /* 1 */
      height: 0; /* 1 */
      overflow: visible; /* 2 */
    }
    
    /**
     * 1. Correct the inheritance and scaling of font size in all browsers.
     * 2. Correct the odd `em` font sizing in all browsers.
     */
    
    pre {
      font-family: monospace, monospace; /* 1 */
      font-size: 1em; /* 2 */
    }
    
    /* Text-level semantics
       ========================================================================== */
    
    /**
     * Remove the gray background on active links in IE 10.
     */
    
    a {
      background-color: transparent;
    }
    
    /**
     * 1. Remove the bottom border in Chrome 57-
     * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
     */
     
     /*  大部分已省略,完整的版本可以查看👉 https://github.com/necolas/normalize.css/blob/8.0.1/normalize.css */
    
  • 优点

    1. 保留了元素的默认语义化样式,h1就是比h2大。
    2. 只修复问题,代码注释清晰,像一本浏览器修复手册。
    3. 它成为了过去十年里,包括Bootstrap、Ant Design在内,无数框架和组件库的基石。
  • 2025年的缺点

    1. 过于保守:它只修复不一致,但并没有提供一些我们现代开发中普遍认为更好的默认值。比如,它就没有设置box-sizing: border-box;
    2. 部分规则已过时:它里面的一些修复,是针对我们现在根本不需要支持的、非常古老的浏览器版本的(比如旧版IE)。

normalize.css在今天,依然是一个安全、稳妥的选择。它不会犯错,但我觉得,它有点不够看了😫。


最佳选择:remedy.css

  • 原理:在normalize.css的基础上,再往前走一步。它不仅修正了不一致,还提供了一套我们现代Web开发中,普遍认为 更好的默认样式

  • 核心特性

    1. 开箱即用的border-box

      *, ::before, ::after {
        box-sizing: border-box;
      }
      

      这几乎是所有现代CSS项目的第一行代码,它帮你写好了。

    2. 更好的响应式媒体元素

      img, picture, video, canvas, svg {
        display: block;
        max-width: 100%;
      }
      

      这能天然地防止图片、视频等媒体元素撑破布局,是响应式设计的基础。

    3. 更平滑的字体渲染和滚动

      html {
        -moz-text-size-adjust: none;
        -webkit-text-size-adjust: none;
        text-size-adjust: none;
        scroll-behavior: smooth;
      }
      
    4. 更友好的可用性/无障碍默认值

      [disabled] {
        cursor: not-allowed;
      }
      
  • 优点:它像一个经验丰富的老手,把你开新项目时,那些不得不写的、或者最好要写的样板代码,都提前帮你准备好了。

  • 缺点:它带有一定的主观性。比如,它默认移除了所有元素的margin,统一用padding来控制间距,这需要你适应它的理念。

对于我们团队的新项目,尤其是那些需要快速启动的中后台项目,remedy.css或者类似的现代Reset方案(比如modern-css-reset),已经成为了我的首选


选择与建议🤞

Reset 类型 哲学思想 适用场景 在2025年的建议
reset.css 简单粗暴的重置 高度定制视觉、几乎没有原生HTML元素的UI 不推荐❌
normalize.css 保留并修正 任何项目,尤其是需要保持浏览器原生感的 安全,但略显保守👍
remedy.css 现代最佳实践 所有新项目,尤其是中后台、需要快速启动的项目 强烈推荐首选👍👍👍
自己定义 量身定制 大型项目、有完整设计系统的团队 终极方案,成本高🤔

CSS Reset 只有权衡,没有什么可选,不可选。

但在2025年,我们权衡的基点,已经从如何抹平IE的差异,变成了如何以一个更现代、更高效、更符合最佳实践的基点,来开始我们的工作

所以,下次当你的新项目npm init之后,别再下意识地npm install normalize.css了。

或许,remedy.css会给你一个更好的开始。

祝大家国庆愉快🙌

❌