1. 控制台不只是 console.logUtilities Function 是藏在 Console 里的另外半张王牌做前端调试这么多年我有一个很深的感受大多数开发者对 DevTools Console 的认知停留在 console.log、console.warn、console.error 这几个方法上。遇到问题就是一顿打点打完点再删掉删完再打循环往复。而 Console 面板顶部那一排看起来像摆设的小函数很少有人正眼看过。我说的就是 Utilities Function中文一般叫控制台工具函数。它跟 console 对象上的 API 是两回事——console.log 是 JS 语法层面的工具而 Utilities Function 是 DevTools 自己注入到 Console 执行环境里的一组快捷函数。它们平时不占用你业务代码的全局命名空间只在调试会话里存在所以很多人开了十年控制台也没察觉它的存在。这一组函数到底能做什么简单说就是四个方面快速拿 DOM 节点、格式化数据输出、追踪事件与函数调用、做性能和内存快照。这篇文章是我你所不知道的各种前端 Debug 技巧系列的第 8 篇专门把这块一次性讲透。适合谁看所有日常用浏览器开发、经常需要在控制台手搓调试代码的前端。你不需要特别深的原理功底只要会打开 Console就能立刻用上。我写这些内容其实不只是想列 API更想把什么时候用哪个、为什么用它、踩过什么坑都讲清楚。因为工具函数这东西说出来每个都简单但真正能在五分钟内解决一个线上问题时你才知道它有多值钱。1.1 为什么 Utilities Function 和 console.log 不是一回事很多人第一次看到这些工具函数时会下意识以为是 console 对象的方法。其实不是。你可以在控制台里输入 console 然后回车看看展开这个对象里面并没有 $、$$、$0 这些属性。它们是被 DevTools 注入到当前执行上下文中的全局函数只存在于 Console 面板的调试环境里。这就带来两个特性。第一你不需要 import 任何东西打开 Console 就能用。第二它们只在调试环境生效不会污染页面本身的全局变量。不过也有一个副作用如果你在业务代码里也定义了同名全局函数控制台里可能就会发生覆盖或冲突这个我们后面单独讲。从底层实现看Chrome DevTools 在初始化 Console 的执行上下文时会往全局对象上挂一批 helper 方法。这批方法查不到标准文档因为它们不是 ECMAScript 规范的一部分也不在 Web API 标准里属于 DevTools 的私房工具。这就导致很多人压根不知道它们存在也导致我见过不少团队的新人调试效率始终上不去。1.2 先建立一张工具函数能力地图我习惯把常用的 Utilities Function 按用途分四类调试的时候直接按图索骥分类常用函数典型场景DOM 获取$0~$4、$、$$、$x快速拿到元素引用批量遍历节点XPath 查询数据展示table、dir、dirxml、keys、values结构化看对象、数组、DOM 树事件与函数getEventListeners、monitorEvents、unmonitorEvents、monitor、debug、undebug排查事件绑定冲突、监听函数调用性能与内存profile、profileEnd、queryObjects、inspect定位性能瓶颈、确认对象实例数量这张表先放着后面每一步都会展开讲。我想先强调一下不要一上来就背函数名而是先建立需求到工具的映射。遇到想看 DOM 结构就去想 dirxml遇到想看数组详情就想 table遇到想知道事件绑了谁就想 getEventListeners。用得多了肌肉记忆自然就形成了。2. 控制台里的 DOM 三板斧$0、$()、$$()、$x() 到底怎么用前端调试里最频繁的动作是什么我的答案永远是拿一个 DOM 节点。传统做法是打开 Elements 面板看半天然后在 Console 里 document.querySelector有时候选择器写复杂了还会选错。Utilities Function 对这件事的优化是降维的。2.1 $0~$4Elements 面板里正在选中的节点早就等你了$0 这个函数我觉得是 DevTools 里最被低估的快捷键。它的含义很简单当前在 Elements 面板里选中的那个节点。你在 Elements 面板里点一下某个 div然后切到 Console输入 $0回车那个 div 的引用直接返回给你。而且不需要你写任何选择器也不会选错。$1 到 $4 是历史记录。$1 是上一次选中的节点$2 是上上次依此类推最多往回翻五步。这个机制特别适合在两个节点之间反复切换、比较属性或者对比样式的时候用。你可以先在 Elements 里点节点 A再去点节点 B然后回 Console 操作 $0 和 $1就能非常快地做差异对比。这里有个很实际的经验如果你在 Elements 面板里没有选中任何节点$0 返回的是 undefined。很多初学者以为 $0 是当前页面 body 下面第一个元素不是的它完全依赖 Elements 面板的选择状态。另一个坑是如果你切换到 Sources 等其它面板再切回来选择状态一般会保留但页面如果执行了重渲染$0 指向的节点可能已经不在 DOM 里了此时对它操作会出现各种奇怪报错调试时要留意。2.2 $() 和 $$()别再用 document.querySelector 敲那么多字母了$ 是 document.querySelector 的简写$$ 是 document.querySelectorAll 的简写。比如你想拿页面里所有类名为 item 的元素输入 $$(.item) 回车就得到一个包含所有匹配节点的数组注意是数组而不是 NodeList这意味着你可以直接 .map、.filter、.forEach非常顺手。与原生 API 相比最大的快感就是少敲十几个字符。但真正实用的是组合用法先 $0 拿到当前元素再 $$(li, $0) 拿到该元素下的所有 li。没错$$ 支持第二个参数指定搜索范围的根节点。这个特性知道的人很少但在做局部 DOM 分析时极其高效。不过要提醒一个常见坑$ 和 $$ 的实现跟页面内的库可能有冲突。比如你页面里引入了 jQuery 或者某些工具库它们也定义了全局 $。Console 里到底用哪个取决于当前执行上下文以及 DevTools 的注入时机。通常 DevTools 的 $ 会优先但如果你在当前页面作用域里定义了同名变量控制台会优先解析到你的变量。遇到这种混乱时我建议直接改用 document.querySelector优先保证精确。2.3 $x()用 XPath 在特定 HTML 结构里降维打击$x 是纯正的 XPath 查询工具。用法是 $x(//div[classcontent])返回匹配的节点数组。XPath 的语法比 CSS 选择器更古老但有些场景优势明显比如你想找某个 div 下前面两个兄弟节点CSS 选择器写起来很别扭XPath 写 //div[classtarget]/preceding-sibling::div[position() 2] 就很干净。我用得最多的场景是爬取表格里的指定行、查找包含特定文本的节点XPath 支持 text() 判断或者绕过那些 class 老是动态变化的组件去定位结构。如果你平时不太写 XPath在 Console 里主要知道三件事就够了// 从根开始//div 递归查找所有 div[条件] 过滤 取属性。这几点覆盖了八成使用场景。$x 返回的结果同样是一个数组而不是 NodeList所以一样可以直接用数组方法。需要注意的一点是XPath 表达式在某些情况下会比 querySelector 慢如果你做的是高频轮询调试而且目标只是 class 查找老老实实用 $$ 反而更快。3. 数据展示组合拳table、dir、keys/values、copy 让对象开口说话Console 最常用的操作就是输出变量。但大多数人只会 console.log遇到嵌套对象直接一坨展开看多了是真头疼。Utilities Function 里有一组专门优化数据展示效率的函数我认为它们对调试效率的提升比任何 console.log 花样技巧都大。3.1 table()数组和对象的高级表格视图table() 的作用是把数组或对象渲染成一张可排序的表格。传入一个对象数组控制台会列出每一列传入一个对象它会列出 key 和 value 两列。这让数据规律一眼可见尤其在处理多个同类对象时比如日志接口返回的十来个用户对象console.log 输出一整块table 输出一张行列表谁对谁错瞬间清楚。table 的第二个参数是要展示的列比如 table(users, [id, name])就只显示这两列。我在排查接口字段缺失时经常这么干先把返回数组 table 出来再缩小列范围对比比肉眼在 Object 树里翻要快得多。但要提醒table 对嵌套过深的对象支持有限单元格里显示 [object Object] 是常态。它适合扁平结构批量查看不适合深层次 JSON。遇到深层嵌套我的做法是先 JSON.stringify再看情况截断而不是指望 table 能帮你展开全部层级。3.2 dir() 和 dirxml()查看对象时比 log 更懂你dir() 是 console.dir 的别名它直接把对象以属性列表的形式展开。和 console.log 有个重要区别log 对 DOM 节点输出的是元素的标签形式dir 输出的是该节点对象完整的属性结构。所以你想看一个元素上有哪些自定义属性、style 对象内部长什么样用 dir($0) 比 log($0) 可靠得多。dirxml() 则反过来它以 XML/HTML 的形式展示节点相当于在 Console 里快速渲染出一段 DOM 树。做模板调试、或者想确认某个节点下有多少层子元素时dirxml 比 Elements 面板更轻快。它支持传入元素或元素数组也能传 XML 文档对象。这三个函数我建议你这样记想看人用 log想看结构用 dir想看父子层级用 dirxml。场景对上了调试速度能提升一个档次。3.3 copy()一键复制对象到剪贴板配合 JSON 处理无敌copy() 能把任意值复制到系统剪贴板。前端日常经常要把接口返回的对象粘贴到某个地方做分析通常的做法是右键查看 → 手动选全 → CtrlC遇到大对象手都酸。用 copy(JSON.stringify(data, null, 2))直接就是格式化的 JSON 文本随便贴到编辑器里看。这个函数配合 JSON.parse 还能玩出很多花样先复制一段 JSON 字符串在 Console 里 JSON.parse 成对象再 copy 格式化后输出整个过程不需要打开任何编辑器直接在网页里完成了文本 ↔ 对象的快速转换。有个细节要提醒copy 复制的是值的字符串表达。如果你 copy 一个 DOM 节点复制出来的是它的 outerHTML如果你 copy 一个函数复制的是函数源码。这个特性其实挺有用比如你想把某个线上页面的完整 HTML 结构存下来做分析直接 copy($0) 就行。3.4 keys() 和 values()对象的快速体检keys() 和 values() 分别对应 Object.keys 和 Object.values。单独看它们似乎没什么存在感但组合起来调试很顺手keys($0) 快速看元素上有哪些属性values($0) 快速看各属性值。特别是某些框架生成的组件实例属性特别多你想弄清楚它内部到底有什么keys 一出来就一目了然。我自己的习惯是配合 JSON 格式化来体检对象keys(obj) 看字段名是否齐全values(obj) 看顺序是否预期再 table(obj) 看整体结构。这一套组合下来绝大多数数据结构问题都能肉眼定位。4. 事件与函数追凶getEventListeners、monitorEvents、debug 排查绑定问题前端线上问题里交互类 bug 通常是最难排查的点击没反应、事件触发两次、某个函数被莫名调用。这种问题靠 console.log 打点效率很低因为你要先猜代码位置再插入日志再复现。Utilities Function 里有一类追踪器专门解决这种摸黑排查的痛点。4.1 getEventListeners()一秒钟查清谁绑了事件getEventListeners(target) 返回目标元素上所有事件监听器结果是对象key 是事件类型value 是监听器函数数组。这里有个踩坑点它只能查通过标准 addEventListener 绑定的监听器如果用了某些框架的合成事件系统比如 React 17 之前的事件委托你在元素上查到的可能只有一层委托逻辑看不到真正的业务回调。这不是函数没用而是事件绑定的具体位置得往 document 层查。实用性最强的是排查事件重复绑定。比如某个交互在上线后点击一次却触发了两次逻辑用 getEventListeners($0).click 看监听器数组的长度如果大于 1基本就能断定有重复绑定。然后你再逐个展开数组里的 listener 属性查看函数体内容或者引用地址找出是哪段代码重复注册的。有一点要注意返回的监听器函数展开后属性里会显示 handler 和 source但如果你是在压缩后的线上代码里调试函数名可能是混淆的直接看很难对应到源码位置。这种情况下可以先把该元素的监听器数量列出来再结合代码搜索确认不要指望光凭这一个 API 就精确定位到行号。4.2 monitorEvents()把用户交互变成日志流monitorEvents(target, events) 会在目标元素上打印事件日志。比如 monitorEvents($0, click)之后每点一次这个元素Console 就会输出一条包含事件类型和 target 的记录。查为什么点击后啥反应都没有时这一步能快速确认事件到底有没有被触发。第二个参数可以传事件类型数组monitorEvents($0, [click, mouseover, keydown])。不过我用得更多的是不传第二个参数全部事件都监听。注意这种模式下日志量会非常大尤其是 mousemove、scroll 这类高频事件一瞬间就能刷屏。我建议先精确定位事件类型再看全量。监听完记得用 unmonitorEvents($0) 取消否则它一直挂着后续操作日志很干扰视线。我之前就吃过这个亏开了 monitorEvents 忘了关排查下一个问题时控制台疯狂输出浪费了不少时间。4.3 debug() 和 monitor()让函数自己暴露行踪debug(function) 会在函数体第一行自动加断点。用法很直接先在 Console 里拿到函数的引用然后 debug(myFunction)之后任何代码调用这个函数执行都会在函数入口停下来直接进入 Sources 面板调试。排查某段逻辑为什么走了这个分支时这比手动打断点更精准因为断点是挂在函数上的调一次断一次。和它配套的是 monitor(function)它不打断点而是在函数每次被调用时往 Console 输出一条调用日志包括参数和返回值。这个函数在确认某个函数到底有没有被调用时极其高效不需要改任何源码也不影响执行流程纯观察。用在第三方库的回调、或者你怀疑被框架包裹的内部方法上都非常顺手。debug 有个边界它只能对全局可访问的函数生效。如果函数在模块作用域里没有暴露到 window你还得想办法拿到引用。常见的做法是在源码里临时挂一个 window._fn fn或者通过断点找到调用它的作用域链。即时这样也比反复改代码打 console.log 要快。5. 性能与内存快照profile 和 queryObjects 定位卡顿与泄漏控制台不只是用来查代码逻辑它还能帮你看运行时状态。这一块我喜欢称之为性能与内存的体检科对应 Utilities Function 里的 profile、profileEnd 和 queryObjects。5.1 profile / profileEnd手动录制一段 CPU 性能数据profile(label) 开始记录一段 JavaScript CPU 性能数据profileEnd(label) 结束记录。结束后Performance 面板会自动出现一段对应的录制结果你可以查看这段时间内函数调用的耗时占比定位到底哪个函数吃得最多。日常调试中我最常用的场景是页面在某个交互后明显卡顿我先刷一下打开 Performance 面板然后回到 Console 执行 profile(卡顿排查)迅速触发交互再 profileEnd(卡顿排查)。这样录制的数据是干净隔离的只包含交互前后的执行区间分析起来不用在一大堆页面初始化记录里大海捞针。标签参数是成对对应的profile 和 profileEnd 的 label 必须一致否则记录不会被正确匹配。另外在较新版本的浏览器里可能更推荐直接在 Performance 面板点录制按钮但 Console 这种方式在精确控制开始和结束时机上依然有优势尤其是你需要从 Console 里触发业务函数时可以做到无缝衔接。5.2 queryObjects()检查某个构造函数的实例数量queryObjects(Constructor) 返回当前页面上通过 new 该构造函数创建的对象实例数组。它的典型用途是排查内存泄漏如果你怀疑某个类创建了大量对象没被回收在 Console 执行 queryObjects(某类)返回数组的长度就是当前还活着的实例数量。我实战中的一个例子是排查一个弹窗组件是否被重复创建在 Console 里 queryObjects(组件构造器)发现数组里有二十多个实例但页面只应该打开一个弹窗这个数量明显不正常。顺着这个线索去查组件销毁逻辑最后发现是关闭弹窗时没移除全局引用导致每次打开都保留了一个实例。这里有个前置条件你必须能在全局访问到那个构造函数。如果构造函数在模块内部没有暴露queryObjects 就查不到。还有一个容易忽略的点queryObjects 查的是当前执行上下文里的对象跨 iframe、跨 context 的情况需要切换到对应的上下文环境再查。5.3 浏览器差异不是每个浏览器都叫 ChromeUtilities Function 是 Chrome DevTools 的强项但并不意味着所有浏览器都给你备齐了。Firefox 的 Web Console 也有类似能力比如 $0、$、$$ 都能用但 XPath 相关的 $x 和部分性能函数并不完全一致。Safari 的 Web Inspector 支持度则更弱一些。我自己的建议是日常开发主力如果是 Chrome把这些函数当成调试母语来练偶尔需要跨浏览器排查时先确认目标浏览器支持哪些再决定用不用。毕竟 Utilities Function 是调试辅助不是业务依赖切换成本并不高。6. 举个完整例子用一串 Console 命令复现并定位一个卡顿 bug说了这么多 API我拿一个曾经在实际项目中排查过的问题把完整链路串一遍。那次是某个管理后台列表页在滚动时越来越卡肉眼可见掉帧。按照我平时的习惯第一反应是加 console.log 查滚动处理函数但这次我全程只用了 Console 的 Utilities Function。第一步先在 Elements 面板选中滚动容器拿到 $0。然后我执行 monitorEvents($0, scroll)确认滚动事件是不是被多次触发——结果发现每滚动一小段scroll 事件就会触发一遍又一遍这是正常的但我在 Console 里看到触发频率高得不正常说明是 handler 太耗时。第二步我需要确认到底哪些函数在往容器上绑东西。执行 getEventListeners($0) 一看scroll 下面挂着两个监听器其中一个是节流后的函数另一个明显是某些组件初始化时直接 addEventListener 的没有做节流。定位到监听器数组后我通过展开 listener 拿到它的引用再执行 debug(那个函数)下一次滚动直接断在函数入口接下去就是 Sources 里单步走看它内部到底做了什么。结果发现里面有一段对列表所有 DOM 节点做样式重算的逻辑复杂度随着列表数量线性上升。第三步为了验证是不是这个函数导致内存压力上升我在 Console 里执行 profile(滚动卡顿)然后滚了几屏再 profileEnd(滚动卡顿)切到 Performance 面板看耗时占比那个函数占了将近 60% 的执行时间基本可以下结论了。最终修复方案是把样式重算移出滚动回调或者做防抖。整个过程没有改动过业务代码一行只是通过 Console 工具函数完成了定位。这套链路的价值在于monitorEvents 帮你确认事件本身正常getEventListeners 帮你找出绑定异常debug 帮你进入函数内部profile 帮你量化耗时占比。四个函数各管一段组合起来就是一个完整的排查流程。这也是为什么我一直建议前端开发者别只盯着 console.log把 Utilities Function 用熟调试思路会完全不一样。6.1 事件重复绑定的另一种快速判定法回到 getEventListeners 查重复绑定的话题如果你想更快一点可以在 Console 里输入 getEventListeners($0).click.length。如果这个值大于 1不要急着怀疑框架先用前面的 debug 方法逐个断进去看如果等于 1那问题大概率不在绑定数量上而是事件对象被 stopPropagation 拦了或者 preventDefault 干扰了后续逻辑。排查这类问题时我还有个习惯先 unmonitorEvents($0) 清掉之前的事件监听日志再重新精确监听目标事件避免之前高频率的日志干扰判断。干净的环境才能得出干净的结论。6.2 从 Utility 到 Performance 面板的衔接我最后想再提一个衔接技巧Utilities Function 并不只是 Console 面板的孤岛工具它们产生的结果可以和 DevTools 其它面板打通。copy 出来的对象可以直接贴到 Sources 里新建的临时脚本里debug 设置的断点会出现在 Sources 面板的断点列表中profile 录制的数据直接进入 Performance 面板。这意味着一套调试动作可以在四个面板之间无缝流转而不需要你频繁手动切换和重复定位。这个系列写到这里我最大的感觉是DevTools 每一代更新都在加新功能但 Console 里这批 Utilities Function 一直稳定存在反而成了被忽略的老朋友。如果你还停留在只会 console.log 的阶段建议从今天这篇开始把 $0、table、getEventListeners 这三样先练熟。它们能覆盖日常调试里至少一半的重复劳动。等你习惯用 copy 和 profile 以后再回头看以前的调试方式会有种回不去的感觉。
阅读完成 · 觉得有帮助?