☰
DevTools Utilities Function:控制台里被忽略的前端调试神器
2026/10/17 18:02:38 网站建设 项目流程

1. 控制台不只是 console.log:Utilities 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~$4:Elements 面板里"正在选中的节点"早就等你了

$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[@class="content"]'),返回匹配的节点数组。XPath 的语法比 CSS 选择器更古老,但有些场景优势明显:比如你想找"某个 div 下前面两个兄弟节点",CSS 选择器写起来很别扭,XPath 写 //div[@class="target"]/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() 能把任意值复制到系统剪贴板。前端日常经常要把接口返回的对象粘贴到某个地方做分析,通常的做法是右键查看 → 手动选全 → Ctrl+C,遇到大对象手都酸。用 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 浏览器差异:不是每个浏览器都叫 Chrome

Utilities 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 以后,再回头看以前的调试方式,会有种回不去的感觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询