写了这么些年前端我最大的感受之一就是绝大多数人根本没把 console 当回事。每天用得最多的三个方法就是 log、warn、error其他的一概没碰过。尤其是字符串拼接日志的时候满屏都是“值为”“结果”这种没有辨识度的输出排查问题全靠肉眼在一堆黑白文字里捞。实际上浏览器把 console 设计成了一整套调试工具箱光 Utilities Function 就有十几个每一个都有明确的适用场景。今天这篇是这个系列的第 8 天集中讲那些常被忽略、但真正能帮你提升调试效率的 console 实用函数。如果你平常只是用 console.log 打印变量看一眼结果那今天的内容绝对值得花十分钟过一遍。我把每个函数拆开讲解释它为什么存在、适合解决什么问题、有哪些坑最后还会给一套可以直接抄的封装思路让你在项目里少写不少重复代码。1. 先把基础打扎实格式化占位符与样式输出很多前端新人写 console.log 的时候习惯这样拼接字符串console.log(用户 userId 在 time 下单金额为 price);这么写的问题很明显变量一多引号和加号满天飞眼睛看过去全是转义符很难一眼挑出变量插在哪。而且如果中间某个变量是 undefined整行日志拼接出来可能直接变成“undefined”排查的时候还得再复制一遍去定位是哪个变量出了问题。1.1 五个常用占位符%s、%o、%c、%d、%i其实 console 系列方法原生支持类似 C 语言 printf 的占位符写法只是很多人不知道或者没用顺手。用法很简单第一个参数传格式字符串后面的参数依次填入占位符的位置占位符作用示例%s字符串console.log(%s 完成了任务, name)%d / %i整数console.log(重试 %d 次, retryCount)%f浮点数console.log(耗时 %f ms, cost)%o / %O可展开的 DOM / JavaScript 对象console.log(%o, domNode)%c后续文字应用 CSS 样式console.log(%c这是红色, color:red)我常用的组合是这样console.log(%s 用户下单成功订单号%d实付金额%f 元, user.name, order.id, order.amount);这种写法最大的好处是模板和变量完全分离日志模板稳定变量位置一目了然后期改成 i18n 或者统一封装也更方便。用了一段习惯之后再回去看那种字符串拼接的写法你会觉得特别累。1.2 用 %c 给日志“上色”可复制的彩色日志函数%c 是很多人忽略的宝藏占位符。它后面的文字会被第二个参数里的 CSS 样式修饰。浏览器 DevTools 控制台支持常见 CSS 属性比如 color、background、font-size、padding、border-radius甚至能用图片做背景base64 编码。我以前排查接口联调问题时写过一个小工具函数热度至今还在用function tagLog(tag, message, color #35495e) { console.log( %c${tag}%c ${message}, background:${color}; color:#fff; padding:2px 6px; border-radius:3px;, color:inherit; ); } tagLog(API, 用户列表请求成功, #409eff); tagLog(CACHE, 命中缓存跳过请求, #67c23a); tagLog(ERROR, token 失效, #f56c6c);效果上控制台里会出现带底色的标签日志优先级一眼就能区分。尤其是接口多、日志刷屏快的场景有颜色区分和没颜色区分的排查效率完全两码事。1.3 占位符的注意点格式串与参数个数占位符虽然好用但有两个坑需要提前说清楚。第一个坑是参数个数不匹配。如果格式串里写了 3 个占位符却只传了 2 个变量漏掉的那个位置浏览器会显示占位符本身比如直接输出“%s”看起来像 bug 而不是日志。反过来如果参数比占位符多多出来的参数也会正常打印出来。所以封装的时候最好保证参数个数和模板一一对应别偷懒。第二个坑是 %o 和普通字符串混用时容易出现 JSON 转字符串的误会。console.log 接收对象参数时浏览器默认以可展开对象的形式展示但在格式串里如果你写成 %s对象会被转成字符串 [object Object]排查时完全看不到内部结构。所以只要想展开对象就得用 %o 而不是 %s。另外补充一点%c 样式在 Node.js 终端里是无效的终端控制台不会解析 CSS。如果写的是服务端调试代码彩色输出基本会变成乱码直接降级成普通文本就好。2. 别再用 console.log 打印数组了结构化的表格与分组技巧打印数组和对象数组的时候我非常推荐先问自己一个问题我要的是“一条日志”还是“一组结构化数据”如果是后者console.log 并不是最优解因为它把所有数据压成一行字段一多根本看不完。这时候 console.table 和 console.group 才是正确的打开方式。2.1 console.table从“数据乱流”到表格视图console.table 的用法很简单const users [ { id: 1, name: 张三, role: admin }, { id: 2, name: 李四, role: editor }, { id: 3, name: 王五, role: viewer }, ]; console.table(users);浏览器控制台会直接渲染成一张表格每一行是一个对象每一列是一个属性。字段多到横着放不下的时候比 console.log 一条 JSON 长字符串直观得多。除了数组console.table 也支持对象作为第一个参数对象的 key 会变成表格的行名。更实用的是第二个参数 columns可以指定只展示哪些列console.table(users, [name, role]);这样表格里只保留 name 和 role 两列id 被过滤掉现场讲给同事听的时候不会被无关字段干扰。我在排查表格数据渲染问题时经常先把接口返回的原始数据竖成表然后对比页面上的表格哪行少了、哪列映射错了一眼就能找到。2.2 console.group给日志套上可折叠的文件夹console.group 解决的是日志分类和层级问题。用法是开启一个分组组内的日志会自动缩进一层调用 console.groupEnd 结束分组。如果不想组内日志默认展开用 console.groupCollapsed 代替 console.group控制台默认折叠显示点开才看内容。我写过一个遍历渲染列表的排查案例console.groupCollapsed(商品列表渲染排查); console.log(接口返回条数, list.length); list.forEach((item, index) { console.log(第${index}条%s价格 %f, item.name, item.price); // 这里还可以嵌套子分组打印更详细的字段 console.groupCollapsed(详情); console.log(库存, item.stock); console.log(SKU 数, item.skuCount); console.groupEnd(); }); console.groupEnd();这么做的好处是控制台不会瞬间刷出几十几百条平铺日志而是折叠成一个标题需要看哪块再展开哪块。排查线上问题时我通常会在关键逻辑入口打一个 group整个请求的生命周期日志全部收进去定位问题像翻文件夹一样顺畅。2.3 console.dir 与 console.dirxml打印对象与 DOM 的正确姿势console.log 在打印 DOM 节点时控制台会以 HTML 标签的形式展示鼠标悬停还会在页面上高亮元素。这在某些场景下好用但如果你想知道这个 DOM 节点有哪些属性、绑定过哪些事件、事件回调长什么样HTML 标签模式就帮不上忙了。这时候用 console.dir它会以“对象树”的方式展开 DOM 节点的完整 JavaScript 属性包括 dataset、style、attributes、甚至内部自定义属性排查事件绑定是否生效特别管用。对应的还有一个 console.dirxml专门用于打印 DOM 元素或 XML/HTML 结构的字符串表示。它会以可展开的标签树形式展示适合看 DOM 嵌套层级和结构是否正常。比如排查一个弹窗插到 body 哪个位置了用 dirxml 比手动去 Elements 面板里找快很多。有一回我排查一个点击事件不生效的问题先 console.log 打印了绑定元素浏览器高亮的是正常节点但怎么点都没反应。后来用 console.dir 展开一看发现元素上有两层引用事件绑在了被替换后的旧节点上新节点根本没绑。这就是 dir/log 差异在实际排查里的价值。3. 四两拨千斤的计量工具count、assert 与 time 系列第 3 部分讲三个特别适合在调试阶段用的计量型函数console.time、console.count、console.assert。它们不是用来“看”数据的而是用来回答三个高频问题这段代码花了多久这段代码到底执行了多少次这个条件到底是不是真的3.1 console.time / timeLog / timeEnd 的三种用法性能排查时我最早用的是 Date.now() 手动算差值const start Date.now(); // 中间一大段逻辑 console.log(耗时, Date.now() - start, ms);后来发现又笨又容易漏因为每次都得引入一个变量、手动做减法。console.time 直接把这套流程压缩成三步console.time(请求耗时); // 模拟异步请求 await fetchData(); console.timeLog(请求耗时, 数据已返回); // 后续处理 processData(); console.timeEnd(请求耗时);console.time(label) 开一个计时器console.timeLog(label) 在计时过程中打印当前累计耗时console.timeEnd(label) 停止计时并输出总时长。三个方法共享同一个标签名console.timeLog 和 console.timeEnd 之间可以穿插任意代码。需要注意标签必须一致。如果你开计时器用的是 A结束用的是 a浏览器会直接提示“No such label”不会帮你自动匹配。第二个注意点是时间单位默认是毫秒但浏览器显示时精度会随耗时变化大耗时自动转成秒或分钟不用自己换算。我在排查首屏性能问题时会在资源加载前、DOM 解析后、图片加载完成后各放一个 timeLog一眼就能看出时间到底耗在哪个阶段。这种连续分段计时是 console.time 最实用的场景。3.2 console.count确认回调有没有重复触发console.count 做的是计数。它在同一个标签下每调用一次控制台就输出一次累积次数。看到输出内容大概长这样count: 1 count: 2我经常拿它排查两类问题一类是事件回调被重复绑定另一类是定时器或轮询被多次启动。比如怀疑某个按钮点击事件绑了两遍btn.addEventListener(click, function () { console.count(click handler); // 业务逻辑 });如果点击一次按钮控制台瞬间跳出两个 count说明这个回调在代码里被重复注册了。比在回调里手动维护一个自增变量更省事因为不需要额外声明计数变量也不会污染作用域。配合 console.countReset(label) 可以重置计数适合那些从某个操作开始重新计数的场景。3.3 console.assert需要“必现断言”时的轻量选择console.assert(condition, message) 接受一个条件表达式条件为 falsy 时才会输出错误信息。它和 if console.error 的区别在于assert 不需要你写 if 分支并且在某些浏览器里它会以 Error 形式展示但不会中断后续代码执行。我经常在前端配置检查里用它console.assert(config.apiBaseUrl, API 地址不能为空); console.assert(typeof config.timeout number, timeout 必须是数字);配置缺失的时候控制台会红字提示但不影响后续代码继续跑适合用来做轻量级启动校验。注意 console.assert 在 Node.js 里的行为有一点不一样当条件为 false 时Node 端会抛出一个 AssertionError如果没被 try/catch 捕获整个进程可能直接退出。所以服务端代码里用 console.assert 前要先确认自己的 Node 版本和行为预期浏览器端则没有这个问题。4. 踩坑与进化引用陷阱、堆栈追踪与内存监控前面讲的基本是“怎么用更舒服”这一节要讲几个很多人踩了都不知道自己踩了的坑以及几个能帮你往前多走一步的函数。4.1 引用对象陷阱console.log 展开的是“当时的我”还是“现在的我”这是 console.log 最经典的坑没有之一。const obj { count: 0 }; setInterval(() { obj.count; console.log(obj); }, 1000);运行这段代码控制台每条日志旁边都有一个可展开的小三角。如果你点开第一条日志去看会发现 count 已经是几十甚至上百了而不是打印那一刻的 0。原因很简单console.log 打印对象时存储的是对象的引用而不是快照。浏览器为了性能不在打印瞬间深拷贝对象等你手动展开时才去读取当前值。所以如果你需要确认对象在某个时间点的确切状态不要直接 console.log(obj)而是把关键字段序列化出来或者打印拆出来的原始值console.log(obj.count); console.log(JSON.stringify(obj));这个坑在排查动态数据变化的问题时特别误导人我第一次遇到时盯着控制台看了十分钟还以为是 setInterval 被触发了多次其实只是引用展开机制在捣鬼。4.2 console.trace拿到完整调用栈console.trace() 的作用是把当前函数被调用的完整调用栈打印出来。有些业务方法被很多地方调用你不知道当前这一条到底是谁触发的这时候 trace 比 log 强得多。比如封装了一个登录弹窗方法function showLoginModal() { console.trace(登录弹窗被触发); // 显示弹窗 }控制台会输出一条调用链显示 showLoginModal 是被哪个函数调用的那个函数又是被哪个调用链拉起来的。排查路由守卫、事件冒泡、组合式 API 互相调用时特别有用省得在代码里一层层往上翻。4.3 console.memory 与性能面板的配合使用console.memory 在 Chrome 里可以查看当前 JS 堆内存用量返回一个包含 usedJSHeapSize、totalJSHeapSize、jsHeapSizeLimit 等字段的对象。这个属性不是标准 API但调试阶段用一用很顺手。排查内存泄漏时我一般会在关键节点连续打几个 console.memory比如一个定时器执行前后各打一次看 usedJSHeapSize 是否只增不减console.log(执行前, console.memory.usedJSHeapSize); // 业务逻辑 console.log(执行后, console.memory.usedJSHeapSize);注意它不是精确的剖面工具只能看个趋势真定位泄漏点还是得配合 Performance 面板做堆快照对比。但作为快速初筛够用了。顺便提一句console.profile 和 console.profileEnd 在部分浏览器里已标记为废弃想记录 JS 执行性能建议直接用 Performance 面板录制功能更完整也更可靠。5. 把 Utilities Function 封装成自己的调试工具箱函数学再多最后都要落到项目里才有效。这一节分享一个我实际用过的封装思路把前面这些技巧整合成一个简单但好用的调试工具模块日常开发直接引入就能用。5.1 从小函数到一套可复用的日志工具箱我在项目里写过这样一个 debug 工具核心思路是分级控制 占位符格式化 样式区分const Debugger (function () { const level window.localStorage.getItem(debug_level) || log; const levels [log, info, warn, error]; function canOutput(method) { return levels.indexOf(method) levels.indexOf(level); } function prefix(tag) { return %c[${tag}]; } function style(color) { return background:${color}; color:#fff; padding:1px 4px; border-radius:2px;; } return { log(tag, ...args) { if (canOutput(log)) console.log(prefix(tag), style(#35495e), ...args); }, info(tag, ...args) { if (canOutput(info)) console.info(prefix(tag), style(#409eff), ...args); }, warn(tag, ...args) { if (canOutput(warn)) console.warn(prefix(tag), style(#e6a23c), ...args); }, error(tag, ...args) { if (canOutput(error)) console.error(prefix(tag), style(#f56c6c), ...args); }, }; })(); Debugger.log(API, 请求完成); Debugger.warn(API, 响应缓慢);通过 localStorage 控制日志级别开发环境默认全量输出线上切到 error 级别就能让 console 只在出问题的时候喊一嗓子不刷屏不干扰。这个思路扩展到 Vue/React 项目里也一样各个模块封装自己的 tag日志按模块过滤排查问题时打开对应模块即可。5.2 生产环境要不要清掉 console怎么清关于生产环境要不要清掉 console我的个人观点是不能一刀切。如果你的线上页面用到了 console 做关键信息展示比如版本号、上报状态那你不能全删。但如果在生产环境里打大量调试日志既拖性能又暴露内部逻辑这时候就建议处理一下。常规做法是用构建工具插件统一移除。比如 Webpack 生态里常见的 babel-plugin-transform-remove-console配置好之后构建产物里不会保留任何 console 语句。如果你用的是 Vite也可以写一个自定义插件在 transform 阶段把 console 调用替换成空函数或者直接把字符串替换掉。不过比“移除”更稳妥的其实是“降级”也就是把 console 重写成一个空实现。这样万一线上要临时复现问题还能通过注入脚本恢复。这个思路在前面那个 Debugger 封装里体现得很自然改一个 level 就行不需要动构建流程。5.3 浏览器差异与兼容性一览最后整理一张表格把我今天提到的这些方法在不同环境下的表现差异列一下免得大家踩坑方法ChromeFirefoxSafariNode.js备注console.log支持支持支持支持通用基础方法console.table支持支持支持支持终端降级为文本Safari 旧版列展示有缺失console.dir支持支持支持支持有限浏览器端查看对象属性更全console.group支持支持支持支持部分终端不缩进console.time支持支持支持支持Node 支持同名计时console.count支持支持支持支持终端显示方式和浏览器不同console.assert支持支持支持支持抛异常Node 行为不同需确认console.trace支持支持支持支持输出格式不同console.memoryChrome 支持不支持不支持不支持非标准仅 Chromium 内核如果你在 Chromium 内核的浏览器里开发这一套函数基本都能直接用如果遇到旧版本 Safari建议先在本地验证一下需要用的函数是否支持尤其是 table 和 group 的展示效果。Node.js 环境则要额外注意 assert 抛异常和样式失效的问题。我个人在实际操作中的体会是console 这些 Utilities Function 单个拆开看都很简单但组合起来能带来很大提升。很多人觉得调试就是打开 DevTools 打个断点就行其实配合 console 的结构化输出、计数和计时很多问题在断点之前就能定位到具体模块。最后再分享一个小技巧调试完了记得把临时日志统一删掉或者在封装的 Debugger 里调高日志级别别让调试代码悄悄跟着上线。