本文目录
急诊分诊时,慌乱的人会看见红灯就全科会诊;熟练的人先问:是外伤出血,还是胸痛缺氧?同样是“告急”,原因不同,处置完全不同。
控制台一整片红字时也一样。真正省时间的不是先改代码,而是先把现象塞进正确的机制格子:是调用栈上的同步异常,还是未处理的 Promise;是 this 绑错了,还是宏任务被长计算挡住;是原型上根本没有这个方法,还是对象已被 GC 收走。
前十七章把机制拆开讲;本章把它们收成一张可对照的调试地图,并练习在 Chrome DevTools 里“点哪里”。读的时候建议并排开着 Sources 与 Console,每遇到一种报错就亲手点一次,别只看截图。你也可以把本章当成系列的“使用说明书”:前面各章回答“世界怎么运转”,这里回答“坏了以后怎么查”。
先猜输出:断在哪一行,栈顶是谁
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 Rejection | async/await、未 catch 的 rejection | Console + Sources(Async) |
| 点了没反应 / 掉帧 | 长任务、事件循环、微任务饥饿 | Performance |
undefined is not a function | this、原型链、可选链误用 | 断点看调用方式与 [[Prototype]] |
| 改了 DOM 却看不见 | 微任务未清完、渲染时机 | Performance + 第四章/第五章模型 |
| 偶发“对象还在 / 监听器泄漏” | 可达性、闭包、未移除监听 | Memory / 弱引用相关思路 |
标签选错,后面两小时都会浪费在错误面板上。标签选对,哪怕暂时不会修,也知道该读系列里的哪一章。养成“先贴标签”的习惯后,和同事同步故障也会更短:你说“这是微任务饥饿导致的掉帧”,对方立刻知道该打开 Performance,而不是去翻 CSS。
DevTools 里真正常用的四个动作
1. 断在异常上,而不是猜行号。 Sources 面板打开 “Pause on exceptions”。能复现的同步错误,让浏览器停在 throw 处,再读右侧 Scope:此时的局部变量比事后 console.log 更接近真相。
2. 条件断点减少噪音。 在循环或高频事件上右键设条件,例如 item.id === targetId。调试的是机制,不是把一千次无关停顿看完。
3. 日志要带队列标签。 排查顺序问题时,统一写成:
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.handler、this、Object.getPrototypeOf(obj),单步时观察它们何时变化。
若 obj.fn 存在但 obj.fn() 里 this 不对,优先回到第三章的绑定规则,而不是先怀疑打包工具。若 obj.fn 本身是 undefined,再去看第十二章的原型链,或确认是不是解构时弄丢了方法。
const api = {
token: 't',
send() {
console.log(this.token)
},
}
const { send } = api
send() // 断在这里看 this:多半不是 api异步调用链:为什么栈“变短了”
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。调试“提交没成功”时,先确认是传输失败、业务码失败,还是前端在成功响应后把状态机写错了——三者对应的机制完全不同。
从一次真实故障倒推机制
假设用户反馈:“点保存以后转圈很久,有时成功有时没反应。”不要立刻改超时时间。按标签拆:
- 卡顿还是挂起? Performance 里若有长任务,先削主线程;若主线程空闲却一直转圈,更像 Promise 没 settle 或 UI 状态没推进。
- 有没有 rejection? Console 过滤
Promise,看是否被空catch吃掉。 - 请求到底结束没有? Network 看 pending / failed / 200。
- 结束之后谁该更新界面? 断在
then/await之后,看是否走进了错误的分支。
四个问题分别指向事件循环、错误处理、模块边界与状态更新。机制地图的价值,就是让你按问题选题,而不是按心情翻文档。写进团队排障清单时,也可以直接复用这四问作为“保存按钮无响应”的标准路径,减少口头传话时的信息损失。
全系列地图:把现象塞回格子
可交互脑图(点击节点可展开/折叠)。调试时把它当成目录:现象归到哪一支,就回到对应章节核对机制,而不是从搜索引擎第一篇教程重读。
表格版便于快速对照:
| 区块 | 章节 | 调试时先问 |
|---|---|---|
| 执行与绑定 | 1–3 | 栈顶是谁?this 怎么来的?名字在哪个词法环境? |
| 调度 | 4–5 | 当前宏任务结束了吗?微任务清完了吗?是否在等渲染? |
| 数据消费 | 6–10 | 迭代有没有提前 return?await 之后是否还在同一调用方? |
| 对象与运行时 | 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;行号对不上时,先用函数名与网络请求交叉验证,而不是死盯压缩后的列号。