首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >资深工程师一眼看穿的 10 种"危险"JavaScript

资深工程师一眼看穿的 10 种"危险"JavaScript

作者头像
前端达人
发布2026-07-14 19:18:11
发布2026-07-14 19:18:11
130
举报
文章被收录于专栏:前端达人前端达人
image
image

代码今天能跑,不代表明天还敢信。

先讲个我遇到的真事。

同事提交了个用户信息卡片,功能全对——头像有了,昵称有了,测试也过了。但我在评论区打了个问号,指着这一行:

代码语言:javascript
复制
const name = user.name || user.full_name || user.profile?.name || '';

我说:你为啥要写四个兜底?

他说:因为有的接口返回 name,有的返回 full_name,还有个老接口塞在 profile 里,不写全会白屏。

那一刻我就知道,这个项目迟早要出事。不是这一行会崩,而是这行代码正在替整个后端擦屁股,而且它绝对不是唯一一个这么干的地方。

这就是老手和新手看代码的区别。新人问"这能跑吗",老手问"这段代码偷偷做了哪些假设,这些假设靠谱吗"。今天咱们就把 10 种"当时看着没毛病、后来越来越坑"的写法,一个一个掰开揉碎讲。每个我都配个生活里的比方,代码也都用真实业务场景,保证你看完能对上号。

📌 小声明:文中代码都是教学示意,为了讲清思路做了精简(省了 AbortController、防抖、错误类定义这些)。重点是理解"为什么这么写",别直接抄进生产项目——真上手时记得补齐这些细节。

一、把数据形状当成"建议"

打个比方:这就像一个前台,来一个访客登记表格式不一样,就临时想一套办法应付。张三的表格写"姓名",李四的写"名字",王五的干脆写在备注里。前台每次都得猜"这次名字在哪一栏"。表格一多,前台就疯了。

前面那个四重兜底就是这么回事。问题不在 ||,而在于**整个项目没有一道"数据进门时的安检口"**。每个页面都在各自防守,出了 bug 谁都说不清数据本该在哪儿被理顺。

老手的做法是:在数据刚进前端那一刻,就把它整理成一个固定、可信的样子,之后所有页面都不用再猜。

代码语言:javascript
复制
// 安检口:脏数据进来 → 干净数据出去,只在这儿理一次
interface User {
  id: string;
  name: string;
  avatar: string | null;
}

function normalizeUser(raw: any): User {
  return {
    id: String(raw.id),
    name: raw.name ?? raw.full_name ?? raw.profile?.name ?? '匿名用户',
    avatar: raw.avatar ?? null,
  };
}
代码语言:javascript
复制
// JS 版,一模一样的逻辑
function normalizeUser(raw) {
  return {
    id: String(raw.id),
    name: raw.name ?? raw.full_name ?? raw.profile?.name ?? '匿名用户',
    avatar: raw.avatar ?? null,
  };
}

不用上什么重型校验库,就记住一句话:接口返回的原始数据 ≠ 你应用里用的数据。跳过这道安检,整个项目就变成一支到处打扫卫生的清洁队。

代码语言:javascript
复制
接口返回(脏、乱、天天变)
        │
        ▼
 ┌──────────────┐
 │  normalize   │  ← 唯一的安检口
 └──────┬───────┘
        │  干净、可信、格式统一
        ▼
   页面A   页面B   页面C   (谁都不用再兜底了)

二、把"算账"和"干活"搅在一起

打个比方:你让会计算一下这单多少钱,结果他不光算了账,顺手还把仓库发了货、给你手机发了条短信、又把订单原件涂改了。下次你只是想问个数,他又把这一整套流程走一遍。你敢再找他算账吗?

代码里就是这样的函数:

代码语言:javascript
复制
function prepareOrder(order) {
  order.total = calculateTotal(order.items); // 算账
  updateCartState(order);                    // 顺手改了购物车界面
  trackEvent('order_prepared');              // 顺手埋了个点
  return order;                              // 还把订单丢回去给别人用
}

名字叫"准备订单",其实它算账、改原件、刷界面、发埋点一条龙。谁调用它,谁就被迫背上这一堆看不见的动作

拆开就清爽了:算账的只管算账(给数据、还数据,不碰外面);干活的只管干活(改状态、发请求、埋点)。两件事都要做,但别搅在一个含糊的名字底下。

代码语言:javascript
复制
// 纯算账:给它订单,还你一个待提交的数据,绝不动外面
function buildOrderPayload(order: Order): OrderPayload {
  return {
    ...order,
    total: calculateTotal(order.items),
  };
}

现在这个函数的承诺很清楚:我只负责把订单整理成能提交的样子。我不刷界面、不发埋点、不改原单。好代码不是函数写得短,而是函数不会背着你偷偷改变世界

三、"图省事"就随手造个 state

打个比方:一份合同,你怕丢,复印了好几份,放在抽屉、放在包里、放在公司、还发了份到邮箱。刚开始挺安心。直到有一天要改一个条款——你得把四份全找出来改对,漏一份就出纠纷。

state 就是这种合同。组件要个值就存一份,表单要副本就拷一份,抽屉要选中项又留一份。同一个东西在好几个地方各活各的,用户一改动,你就得同步四个地方,还得同步对。

举个真实场景:一个订单列表页,筛选条件(时间、状态)到底存哪儿?

代码语言:javascript
复制
// ❌ 合计金额也存一份 state,跟明细各活各的,迟早对不上
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);

// ✅ 能算出来的就别存,让它永远跟着明细走
const [items, setItems] = useState([]);
const total = useMemo(
  () => items.reduce((sum, i) => sum + i.price * i.count, 0),
  [items]
);

别再问"这个值放哪儿方便",要问:这个值到底归谁管。该刷新还能恢复的(比如筛选条件),放 URL;本来就归后端的,别在前端存副本;能从别的数据算出来的(比如合计、勾选数量),压根就不该存。归属清楚了,应用就"安静"了。

四、写"不管时间"的异步代码

打个比方:你在餐厅同时点了三家外卖,谁先到不一定。结果你家门口只认"最后按门铃的那个",先到的那份直接被拒收。你的界面也一样——用户手快点了好几次,或者切来切去,最后是哪个请求的结果把屏幕刷了,全看运气

真实场景:搜索框,用户一边打字一边触发请求。

代码语言:javascript
复制
const result = await searchApi(keyword);
setList(result); // 看着没问题

它从没问过一句:结果回来的时候,用户还在搜这个词吗?很可能用户已经改了关键词,但一个"慢半拍"的旧请求后回来,把新结果盖掉了——于是你搜"手机",界面却显示"手表"的结果。

await 只保证"办完了",它**不保证"现在还需要"**。老手看异步,不只问"它成功了吗",而是问"成功之后,界面凭什么就该信它"。

代码语言:javascript
复制
// 加个"当前有效"的判断:旧请求回来直接作废
useEffect(() => {
  let alive = true;
  searchApi(keyword).then((result) => {
    if (alive) setList(result); // 用户已经改词了就别刷
  });
  return () => { alive = false; };
}, [keyword]);
代码语言:javascript
复制
      用户连续打字(触发好几次)
            │
    ┌───────┼────────┐
    ▼       ▼        ▼
 搜"手" 搜"手机" 搜"手机壳"   (谁先回来不一定)
    │       │        │
    └───────┴────────┘
            ▼
     只有"最新那次"
     才允许刷新界面

保存按钮被点两次会不会生成两笔订单?token 跑着跑着过期了怎么办?这些都是同一个道理:时间也是逻辑的一部分,看着干净的异步代码照样能出乱子。

五、把对象当成"看不见里面的黑袋子"

打个比方:同事递给你一个黑色塑料袋说"帮我把这个交上去"。里面装的啥?要不要拆掉某些东西?有没有过期的?你全不知道,只能整袋交上去,出了事还得背锅。

代码里到处是这种黑袋子:dataoptionsconfigpayload——没人说得清里面哪些字段必填、哪些是给前端用的、哪些是半年前某个页面临时塞的。

代码语言:javascript
复制
function submitForm(data) {
  return api.post('/submit', data); // data 里到底有啥?天知道
}

小项目还能扛,项目一大就成了 bug 窝。更好的做法是把袋子打开,明明白白拼出来要交什么

代码语言:javascript
复制
// 提交发票:清清楚楚,只放该发给后端的字段
function buildInvoicePayload(form: InvoiceForm): InvoicePayload {
  return {
    customerId: form.customerId,
    dueDate: formatDateForApi(form.dueDate), // 日期格式转成后端要的
    items: form.items.map(buildInvoiceItemPayload),
    // 那些只给前端用的字段(比如临时选中态),到这儿就被挡掉了
  };
}

代码是长了点,但它诚实——它告诉下一个人:到底要交什么、前端的样子怎么变成后端的样子。表单数据、接口参数、权限对象、缓存 key 这些跨门交接的东西,别用黑袋子装

六、把业务规则藏进"随手写的 if"

打个比方:公司规定"只有主管能审批 5 万以上的报销"。这条规定要是没写进制度手册,而是靠每个前台各自记——A 前台记成 5 万,B 前台记成 3 万,C 前台干脆忘了。同一条规矩,执行起来五花八门。

代码里最常见:按钮该不该禁用、能不能导出、这个字段必不必填,规则被随手塞进 JSX 或者某个点击事件里。能用——直到别的地方也要用这条规则,于是复制、微调、遗漏,同一条规矩在好几处各写一遍还不一样。

代码语言:javascript
复制
// ❌ 规则散在按钮里,换个页面又得重写一遍,改一处漏三处
<button disabled={user.role !== 'owner' || invoice.status === 'paid'}>
  审批
</button>

// ✅ 把规则单独拎出来,给它一个"家"
function canApproveInvoice(user: User, invoice: Invoice): boolean {
  return user.role === 'owner' && invoice.status !== 'paid';
}

// 按钮只负责"长什么样",不负责"定规矩"
<button disabled={!canApproveInvoice(user, invoice)}>审批</button>

按钮可以决定禁用时灰不灰,但它不该是唯一知道"这事到底准不准干"的地方。还有一条铁律:前端只负责引导用户,真正的权限必须后端再拦一道——前端的判断随时能被绕过。

七、helper 越攒越像"家里那个杂物抽屉"

打个比方:家里都有个抽屉,一开始放剪刀、胶带、充电线。慢慢地,钥匙、发票、药、螺丝、不知道哪来的遥控器全往里塞。最后你要找东西,得把整个抽屉翻个底朝天。

utils 文件夹就是这个抽屉。一开始很干净——格式化日期、拼 query、安全 parse JSON。后来开始塞业务:权限判断、计费规则、报表专属逻辑……突然它就不是工具库了,成了"没人愿意好好安置的逻辑"的垃圾场。

判断很简单:真正的工具函数应该是"无聊"的,它解决一个通用的技术小问题,不需要懂业务。formatDate(格式化日期)是工具;canApproveInvoice(能不能审批发票)就不是——那是业务规则,该待在业务代码旁边。危险的不是"有 helper",而是用 helper 来逃避"这段逻辑到底该放哪"这个问题

八、为了"秀操作"牺牲"看得懂"

打个比方:同样是指路,有人说"往前走两个红绿灯,右拐看到超市就到了",有人说"沿最优路径穿过三个拓扑节点抵达终点"。后者听着高级,但你根本不敢照着走。

炫技的代码就是第二种。一个漂亮的一行流、一条优雅到看不懂的数组链——问题不在于它高级,而在于它省了几行代码、却让读的人多花十倍脑子。而代码往往是在"最要命的时候"被读的:线上出 bug 时、新人接手时、你自己半年后回来改时。

下面两段做的是同一件事(筛掉未激活用户 → 算个 score → 按 score 建索引),但读起来天差地别:

代码语言:javascript
复制
// 秀操作版:三步挤成一坨,key 是啥、每步干了啥全得脑补
const byScore = users
  .filter(u => u.active)
  .map(u => ({ ...u, score: u.value * 2 }))
  .reduce((acc, u) => (acc[u.score] = u, acc), {});
// 读的时候:🤯 得在脑子里跑一遍

// 无聊但看得懂版:每一步都有名字,一眼就知道在干嘛
const active  = users.filter(u => u.active);
const scored  = active.map(u => ({ ...u, score: u.value * 2 }));
const byScore = keyBy(scored, 'score'); // keyBy 来自 lodash
// 读的时候:😌 每步都有名字

简单代码不等于新手代码。所谓简单,是这段代码里已经没有东西需要藏着掖着了。好代码读完你会想"这不挺显然吗"——这不是没水平,这恰恰是目的。

九、错误 catch 了,但"到底啥错"给弄丢了

打个比方:家里电器坏了,无论是没插电、跳闸、还是真烧了,维修工都只回你一句"反正是坏了"。你既不知道自己能不能顺手解决,也不知道该不该叫人来。

代码里最偷懒的写法就是这样:

代码语言:javascript
复制
try {
  await saveRecord(values);
} catch {
  showError('出错了'); // 啥错?不知道
}

可"手机号没填对"和"你没权限保存"和"数据被别人改了",是完全不同的三件事,就该给用户完全不同的提示:

代码语言:javascript
复制
// ValidationError / PermissionError / ConflictError 是你自己定义的错误类型,
// 通常在接口层根据后端返回的状态码(如 422 / 403 / 409)分别抛出
try {
  await saveRecord(values);
} catch (err) {
  if (err instanceof ValidationError) return showFieldErrors(err.fields);  // 标红哪个框填错了
  if (err instanceof PermissionError) return toast('你没有权限保存这条数据');
  if (err instanceof ConflictError)   return toast('数据已被他人修改,请刷新后重试');
  logError(err); // 意料之外的错,留足线索好排查
  toast('系统开小差了,请稍后再试');
}

好的错误处理不只是"别崩",而是帮用户搞清楚下一步该干嘛catch 不是垃圾桶,它是一个要做判断的地方

十、只跑通"一切顺利"的那条路

打个比方:你把新房验收了,水电通、门能开、灯能亮,就签字入住了。可你没试过:同时开空调和微波炉会不会跳闸?下暴雨会不会漏水?这些不是"故意找茬",而是迟早会发生的日常

代码也一样。真实用户会双击、会刷新、会断网、会开好几个 tab、会拿着过期数据提交表单——他们不像你本地测试时那么乖。如果你的代码只在用户"完美配合"时才能跑,那它还没准备好上线。

不用给每个功能上企业级防御,但那些一眼就能想到的失败情况,应该是提前设计好的,而不是等用户投诉才发现的

接口很慢、转圈半天时,界面卡死还是有提示?列表返回为空,是白屏还是"暂无数据"?用户没权限,是报错还是友好引导?保存按钮点两下,会不会生成两笔订单?

这些不是新手才操心的事,这些就是线上每天在发生的事。能跑的代码和靠谱的代码,差别往往就在于——它有多认真地对待那些"不顺利"的时刻。

说到底,更好的 JavaScript 是啥感觉

它未必看着更高级,反而往往看着更省心、更安静

数据进门先过安检;算账的和干活的分开;每个 state 都知道归谁管;异步结果会认"现在还需不需要";对象不再是黑袋子;业务规则有自己的家;工具函数保持无聊;错误保留了"到底啥错";代码宁可无聊也要看得懂;顺利的路能走,不顺利的路也没被无视。

这样的代码,别人 review 更省心,出 bug 更好查,加功能不用东改西改,最重要的是——它不押注"一切都完美"这件根本不可能的事

这就是经验最终教会人的那个转变:

代码语言:javascript
复制
新手写 JS,心里问:  "这能跑吗?"
老手写 JS,心里问:  "这段代码做了哪些假设,这些假设靠谱吗?"

就这一个问题,会悄悄改变你写代码的每一个选择。它让你的代码少一点魔法——而少一点魔法的代码,才更好维护。

JavaScript 灵活到几乎能让你为所欲为,这是它的强大,也是它的陷阱。你可以用松散的数据、随手的 state、看不透的黑袋子一路狂奔,爽一阵子。但未来总会到来:有人改需求、有人复用你的函数、有人开两个 tab、有人传来奇葩数据、有人在没有你记忆的情况下 debug 你的代码。

到那时候,当初图省事写的代码,就会一口一口咬回来

所以别再写只能证明"一切顺利"的代码了。目标从来不是完美,而是——项目长大之后,它依然让人敢信

💬 聊两句

这篇聊了 10 种"当时没毛病、后来变坑"的写法。我最想问你:

你维护过真实项目之后,戒掉了哪个 JavaScript 习惯?

是那个四重兜底的 ||,是 catch {} 里那句万能的"出错了",还是某个你后来死活看不懂的"一行流"?评论区聊聊你的翻车现场,最有共鸣的几个,我下一篇引用 👀

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、把数据形状当成"建议"
  • 二、把"算账"和"干活"搅在一起
  • 三、"图省事"就随手造个 state
  • 四、写"不管时间"的异步代码
  • 五、把对象当成"看不见里面的黑袋子"
  • 六、把业务规则藏进"随手写的 if"
  • 七、helper 越攒越像"家里那个杂物抽屉"
  • 八、为了"秀操作"牺牲"看得懂"
  • 九、错误 catch 了,但"到底啥错"给弄丢了
  • 十、只跑通"一切顺利"的那条路
  • 说到底,更好的 JavaScript 是啥感觉
  • 💬 聊两句
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档