K 的一隅

JavaScript JavaScript 核心机制

控制台红了,下一步点哪里:用机制地图做调试

从急诊分诊出发,把 DevTools 的栈、断点、异步调用链与 Performance 对上前十七章的机制格子。

8 分钟阅读 更新于 2026-07-28
本文目录

急诊分诊时,慌乱的人会看见红灯就全科会诊;熟练的人先问:是外伤出血,还是胸痛缺氧?同样是“告急”,原因不同,处置完全不同。

控制台一整片红字时也一样。真正省时间的不是先改代码,而是先把现象塞进正确的机制格子:是调用栈上的同步异常,还是未处理的 Promise;是 this 绑错了,还是宏任务被长计算挡住;是原型上根本没有这个方法,还是对象已被 GC 收走。

前十七章把机制拆开讲;本章把它们收成一张可对照的调试地图,并练习在 Chrome DevTools 里“点哪里”。读的时候建议并排开着 Sources 与 Console,每遇到一种报错就亲手点一次,别只看截图。你也可以把本章当成系列的“使用说明书”:前面各章回答“世界怎么运转”,这里回答“坏了以后怎么查”。

先猜输出:断在哪一行,栈顶是谁

js
function leaf() {
  throw new Error('空指针')
}

function middle() {
  leaf()
}

middle()

leaf 里抛错时,Call Stack 顶部通常是 leaf,其下是 middle,再下是全局脚本或事件回调。调试器停住的那一行,并不总是你“感觉出错”的业务入口——它是当前执行栈最深的一帧

若改成 Promise.resolve().then(() => { throw new Error('微任务里炸了') }),默认 Call Stack 往往较短;需要打开 Async 相关选项或看 Promise 的 stack,才能把 .then 登记处与真正抛错处连起来。同步路径与异步路径,在面板上的“形状”本来就不同。

现象分类:先贴标签,再开刀

遇到问题时,先用一句话给现象贴标签,再决定开哪个面板:

现象标签优先怀疑优先打开
控制台红字 + 同步 Error调用栈、throw、未捕获异常Sources → Call Stack
控制台红字 + Unhandled Promise Rejectionasync/await、未 catch 的 rejectionConsole + Sources(Async)
点了没反应 / 掉帧长任务、事件循环、微任务饥饿Performance
undefined is not a functionthis、原型链、可选链误用断点看调用方式与 [[Prototype]]
改了 DOM 却看不见微任务未清完、渲染时机Performance + 第四章/第五章模型
偶发“对象还在 / 监听器泄漏”可达性、闭包、未移除监听Memory / 弱引用相关思路

标签选错,后面两小时都会浪费在错误面板上。标签选对,哪怕暂时不会修,也知道该读系列里的哪一章。养成“先贴标签”的习惯后,和同事同步故障也会更短:你说“这是微任务饥饿导致的掉帧”,对方立刻知道该打开 Performance,而不是去翻 CSS。

DevTools 里真正常用的四个动作

1. 断在异常上,而不是猜行号。 Sources 面板打开 “Pause on exceptions”。能复现的同步错误,让浏览器停在 throw 处,再读右侧 Scope:此时的局部变量比事后 console.log 更接近真相。

2. 条件断点减少噪音。 在循环或高频事件上右键设条件,例如 item.id === targetId。调试的是机制,不是把一千次无关停顿看完。

3. 日志要带队列标签。 排查顺序问题时,统一写成:

js
console.log('[sync]', '点单结束')
queueMicrotask(() => console.log('[micro]', 'promise.then'))
setTimeout(() => console.log('[macro]', 'timeout'), 0)

标签比颜色更可靠:颜色会花,标签能直接对照第四章动图。

4. Performance 录一段“复现窗口”。 点一次卡顿操作,停止录制后找红色长任务块:它占用的是主线程,定时器与点击只能排队。看到长任务,再决定是切片、requestIdleCallback,还是 Worker——那是第十三章的边界。

四个动作可以记成口诀:先停住,再看栈;异步要开 Async;顺序靠标签;卡顿去录制。 口诀不替代理解,只是减少手忙脚乱时点错面板。

作用域与 Watch:值“变成 undefined”时看什么

this 错了和变量根本不在当前词法环境,症状都可能是 undefined,但面板上的证据不同。断在可疑行之后:

  • 看 Scope 里的 Local / Closure / Script:名字在不在、是不是被暂时性死区挡住;
  • 看 Call Stack 选中不同帧时 Scope 如何变化——闭包保存的是环境,不是“当时打印出来的快照”;
  • 对表达式设 Watch,例如 obj.handlerthisObject.getPrototypeOf(obj),单步时观察它们何时变化。

obj.fn 存在但 obj.fn()this 不对,优先回到第三章的绑定规则,而不是先怀疑打包工具。若 obj.fn 本身是 undefined,再去看第十二章的原型链,或确认是不是解构时弄丢了方法。

js
const api = {
  token: 't',
  send() {
    console.log(this.token)
  },
}

const { send } = api
send() // 断在这里看 this:多半不是 api

异步调用链:为什么栈“变短了”

js
async function load() {
  const res = await fetch('/api')
  if (!res.ok) throw new Error('接口失败')
  return res.json()
}

load() // 故意不 await、不 catch

这里的失败常常表现为 unhandledrejection,而不是经典的同步未捕获异常。调试器里若只盯着同步 Call Stack,会觉得“栈丢了”。打开异步栈相关显示,或在 load 入口与 await 后各打一断点,能看到:等待前后不在同一段连续栈帧上,但因果链仍在。

第十章与第十七章已经说明 await 把 rejection 转成可 catch 的异常;调试时要养成第二反应:红字是 Error 还是 Unhandled Rejection?两种提示对应两套入口。

网络面板也要一起看:fetch 失败时,有时业务层已经 catch 并转成友好文案,控制台未必再红,但 Network 里仍是 4xx/5xx。调试“提交没成功”时,先确认是传输失败、业务码失败,还是前端在成功响应后把状态机写错了——三者对应的机制完全不同。

从一次真实故障倒推机制

假设用户反馈:“点保存以后转圈很久,有时成功有时没反应。”不要立刻改超时时间。按标签拆:

  1. 卡顿还是挂起? Performance 里若有长任务,先削主线程;若主线程空闲却一直转圈,更像 Promise 没 settle 或 UI 状态没推进。
  2. 有没有 rejection? Console 过滤 Promise,看是否被空 catch 吃掉。
  3. 请求到底结束没有? Network 看 pending / failed / 200。
  4. 结束之后谁该更新界面? 断在 then / await 之后,看是否走进了错误的分支。

四个问题分别指向事件循环、错误处理、模块边界与状态更新。机制地图的价值,就是让你按问题选题,而不是按心情翻文档。写进团队排障清单时,也可以直接复用这四问作为“保存按钮无响应”的标准路径,减少口头传话时的信息损失。

全系列地图:把现象塞回格子

可交互脑图(点击节点可展开/折叠)。调试时把它当成目录:现象归到哪一支,就回到对应章节核对机制,而不是从搜索引擎第一篇教程重读。

表格版便于快速对照:

区块章节调试时先问
执行与绑定1–3栈顶是谁?this 怎么来的?名字在哪个词法环境?
调度4–5当前宏任务结束了吗?微任务清完了吗?是否在等渲染?
数据消费6–10迭代有没有提前 returnawait 之后是否还在同一调用方?
对象与运行时11–14方法在原型哪一层?消息有没有结构化克隆丢东西?对象还可达吗?
拦截与收束15–16访问是否经 Proxy?错误是同步栈还是 rejection?
调试实践17现象标签对了吗?该开 Stack、Async 还是 Performance?

以后再遇到怪异现象,可以按这个顺序自问:

  • 是词法环境还是原型链上的名字?
  • 这次 this 怎么绑定的?
  • 当前是宏任务还是微任务队列?
  • 对象从根是否仍可达?
  • 访问是否经过 Proxy?
  • 错误是同步栈还是未处理的 Promise?
  • 卡顿对应哪一段长任务?

容易踩的坑

误解一:多打 console.log 就等于会调试。 日志能证明某行执行过,却很难说明当时的绑定与队列阶段;断点与栈面板补的是“现场”。

误解二:红字一定是业务逻辑写错。 也可能是未 await 的 async、被吞掉的 rejection、或扩展程序注入的脚本。先看错误类型与栈入口文件。

误解三:Performance 里没有红条就表示不卡。 主线程空闲但合成/GPU 繁忙时,体感仍可能掉帧;前端日常仍先查 JS 长任务,再决定是否深挖渲染管线。

误解四:看懂机制地图就能一次定位所有 bug。 地图减少的是“走错房间”的概率;具体修复仍要读当前仓库的约束与复现路径。

误解五:Source Map 开着就永远可信。 某些 loader、内联脚本或第三方包没有 map;行号对不上时,先用函数名与网络请求交叉验证,而不是死盯压缩后的列号。