ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

别只会用console.log了!console对象调试技巧实战指南

别只会用console.log了!console对象调试技巧实战指南 写JavaScript的人几乎每天都要和console.log打交道。但据我观察大多数人调试日志的姿势长期停留在“一个变量打一行、字符串全靠加号拼”。这不叫会用console.log只能算会用浏览器自带的打印按钮。我自己早期也在这个上面栽过跟头——线上排查问题日志打了六七十行关键对象全被拼成了[object Object]查了一晚上才发现是数据字段对不上根源就是日志根本没把信息打出来。console这个全局对象其实远不止console.log一种用法旁边那群兄弟API配合起来能把调试效率拉高一个档次而且大部分技巧学完就能用。这篇文章梳理一下我在浏览器和Node.js两个环境里实际验证过的console技巧既适合刚入门的新手也适合写了几年还停留在console.log(变量)的老同学。每一条都附了应用场景和避坑提醒可以直接照用到你的项目里。1. 多参数与格式化占位符先治最常见的拼字符串毛病1.1 多参数传入为什么加号会让日志失灵很多新手拿到对象第一反应是console.log(用户信息 user)结果控制台输出一行让所有人崩溃的文字用户信息[object Object]。原因很简单加号触发了隐式toString()普通对象的toString()返回的就是[object Object]你关心的字段全都看不见。数组也一样列表 arr会把数组转成逗号分隔的字符串遇到嵌套数组和对象直接塌陷。正确的做法是把参数分开传console.log(用户信息, user)。这时候浏览器和Node都会把你传的每个参数当成一次独立渲染对象保持对象的形态控制台里可点击展开Node里则会用util.inspect的格式输出带缩进的结构。别小看这一点排查接口返回值、组件props时能展开一个对象和看到一行[object Object]差别就是能不能定位问题的区别。我自己的习惯是在团队里推广一条约定日志里涉及对象、数组、函数、DOM节点一律用逗号分隔传参禁止用加号拼接。这个约定几乎不增加学习成本但对日志可读性的提升立竿见影。真需要把对象转成字符串的场景用JSON.stringify(obj, null, 2)显式序列化而不是让它被隐式转换。1.2 格式化占位符模板化日志的底层支撑除了多参数浏览器控制台和Node还支持一套格式化占位符。很多前端同学知道%c可以加样式却忽略了其他几个。我实际用过的占位符整理如下占位符含义示例%s字符串console.log(%s来了, 张三) 输出“张三来了”%d / %i整数数字console.log(数量%d, 3.7) 输出“数量3”自动截断%f浮点数console.log(价格%f, 19.9) 输出浮点数值%o对象 / DOM节点浏览器会渲染成可展开的结构%O对象宽泛偏对象属性视角的展开%cCSS样式给后续文本套用样式详见第5章%%字面量%号想输出%本身时使用看着简单用起来有讲究。比如我想在日志里统一记录请求节点可以写console.log(%s 请求开始id%d来源%s, POST /api/order, 1024, checkout)这样所有请求日志都走同一个模板后续在控制台按格式过滤、在日志系统里做grep会非常方便。团队如果定了日志规范占位符能让关键字段的格式保持一致不至于有人打id1024有人打id 1024有人打订单号是1024。需要留意两个坑一是占位符参数顺序必须和模板顺序一一对应写错位置会输出莫名其妙的结果二是%c在Node.js里不生效服务端日志就不要指望它加颜色了。另外占位符适合“格式化文本”不适合“展示复杂对象”碰到对象还是老老实实用多参数传参。2. 数据可视化输出table、dir、group 把日志变成面板2.1 console.table数组和对象一眼扫出问题先看一个场景。接口返回用户列表你用console.log(list)控制台输出一堆折成很多行的对象眼睛很难比对某个字段。换成console.table(list)同样的数据瞬间变成表格每行一条记录每列一个字段字段的差异一眼就能看出来。尤其是列表数据量大的时候表格才是人眼扫描最快的形式。const users [ { id: 1, name: 张三, status: active }, { id: 2, name: 李四, status: blocked }, { id: 3, name: 王五, status: active } ] console.table(users)输出是一张三列表格行号就是数组索引。如果某个字段不关心还可以传第二参数指定列console.table(users, [name, status])只显示这两列表格更短更清晰。对于普通对象console.table输出的是key/value两列适合快速查看配置对象。实际经验在处理后端返回的列表数据时我几乎必用console.table。字段多的时候指定columns拍平配合浏览器自带的表头排序能很快找到某条记录的异常状态。但注意表格里遇到嵌套对象会显示成对象字符串遇到函数、undefined字段基本就是空的所以它适合“宽而浅”的数据不适合“深”的数据。2.2 console.dir把深层对象彻底展开console.log打印一个深嵌套对象默认展开的层级有限很多时候要手动点好几层才能看到目标字段。console.dir就是为这个场景准备的。浏览器里console.dir(obj, { depth: null })可以把对象所有层级都铺开Node里也支持第二个参数比如console.dir(obj, { depth: 2 })限制展开到两层。我通常在排查“某个对象内部到底长什么样”时用dir尤其是那些来自第三方库的配置对象或React/Vue的组件props结构往往藏得很深。console.log点五层才能看到的值console.dir一步到位。唯一要注意的是depth设为null或很大的值输出会非常长特别是在浏览器里会占用大量控制台空间定位完问题记得清理。2.3 console.group给日志分组不再平铺一堆一次请求会打出几十条日志全平铺在控制台根本分不清先后和归属。console.group和console.groupEnd可以解决这个问题。调用console.group(订单流程)之后后续所有日志会缩进一层形成一个可折叠的分组直到console.groupEnd()结束。groupCollapsed默认折叠适合把大量细节日志收纳起来需要时再展开。具体场景我这么用请求发出前开组打印请求参数响应回来后打印状态码和处理结果最后groupEnd。这样一个请求对应一个组多个请求在控制台里就是多个独立块。循环处理数据时也可以用groupCollapsed给每条数据开一个折叠组平时不看细节出错时单独点开那条就够。分组还有一个隐藏好处可以嵌套。外层group(请求)内层group(响应头)层级关系在控制台里直接成了树形结构。这比用字符串前缀模拟层级要直观得多。3. 条件判定与调用链追踪assert、count、trace 的救场时刻3.1 console.assert断言失败才输出还不打断运行调试数据校验时常见写法是if (res.code ! 200) { console.error(接口返回异常, res) }浏览器里可以简写成一行console.assert(res.code 200, 接口返回异常, res)console.assert的第一个表达式为false时才输出错误日志为true时什么都不做。它不会抛出异常不会中断代码执行所以非常适合在调试阶段做“软校验”。我看到很多人在写if console.error信息量一样代码却多出三行。这里必须敲黑板浏览器和Node.js对这一条的实现不一致。浏览器里断言失败只是打印一条错误日志但Node.js里断言失败的行为可能直接抛出一个AssertionError直到进程退出除非你用try/catch接住。所以在Node脚本里使用console.assert前先跑一行代码确认你当前Node版本的语义不想踩这个雷就直接用if console.error代替。这个坑我见过不止一次——本地写着写着进程突然退出还不知道是哪行日志干的。3.2 console.count数一数这段代码到底执行了几次有些问题不容易复现比如某个事件回调被触发了两次或者某个定时器一直在跑。普通console.log会刷屏而且不显示次数。console.count(事件触发)会在每次执行时递增并打印“事件触发: 1”“事件触发: 2”这样的计数。想重置就调用console.countReset()。我用过的一个经典场景同事反馈下拉框的change事件偶尔不响应我在事件回调的开头加了一行console.count(change回调)然后在控制台反复操作很快就发现某次点击回调被触发了两次第二次又做了一遍初始化反而把状态改坏了。没有count这种问题只能靠肉眼数日志。注意count的标签是全局累加的同一个标签在不同函数里也会互相计数。如果只是单次调试用个不重名的标签即可。3.3 console.trace回答“你从哪里来”的问题当一段公共函数被十多个地方调用而数据在某一次突然不对时最烦人的问题是这次是谁调用了它console.log只能告诉你函数的内部状态却给不了调用来源。console.trace()会打印当前调用栈也就是从入口一路到这一行的函数调用链。我在公共的格式化函数里加过console.trace(formatData called)然后在本地复现异常场景。控制台里立刻显示调用链找到那个少传了一个参数的调用方整个过程不到五分钟。这比在十多个调用点逐个加日志快太多了。trace的输出比较长适合临时排查用完就删。浏览器和Node.js都支持这个APINode下的输出会是堆栈帧列表同样能看出调用路径。4. 性能计时与Node.js差异time家族确认耗时stdout暗藏坑4.1 console.time / timeEnd测代码耗时的标准姿势想知道一段代码跑了多久很多人会写下开始时间和结束时间然后相减。console.time(label)加console.timeEnd(label)把这个过程封装好了同一label配对出现timeEnd时自动打印“label: xxxms”格式美观还不用自己维护变量。console.time(parseJSON) const data JSON.parse(rawText) console.timeEnd(parseJSON)中间想记录某个阶段可以再调console.timeLog(parseJSON)打印当前累计耗时不会终止计时器。多个计时器可以用不同label并行互不干扰。需要注意同一label不要重复开启否则会收到警告。我比较两个数据处理方案的耗时就用两个不同label分别计时肉眼就能看出差距。相比手写Date.now()差值还要小心时区和精度问题console.time这套API显然是更省事的选择。当然追求更高精度可以用performance.now()日常耗时对比console.time完全够用。4.2 Node.js环境差异console.log写向stdoutconsole.error写向stderr浏览器和Node.js的console对象大体上是同构的但细节差异值得注意。能力浏览器 DevToolsNode.jsconsole.assert 失败打印错误日志不中断可能抛出AssertionError并中断进程%c 样式支持不支持console.table支持支持console.time支持支持输出目标DevTools控制台stdout / stderr在Node里console.log输出到stdoutconsole.error输出到stderr这也是日志系统标准的分流方式stdout收业务日志stderr收错误日志。但有一个隐藏问题当stdout指向管道或文件而不是终端时console.log的写入是异步的。程序异常退出、进程崩溃时最后那几行日志有可能来不及写完这会让线上排查时丢失关键信息。如果对日志完整性要求高可以在关键节点用类似fs.writeSync(process.stdout.fd, msg)的方式做一次同步兜底或者干脆在业务里接入专门的日志库。我个人的建议是短生命周期的脚本、异常退出的边缘场景别只依赖console.log至少给错误路径加一个同步落盘的处理。4.3 循环与高频日志的性能陷阱console.log本身不便宜每次调用都要走格式化、渲染、IO多道工序。循环里贴console.log数据量大时浏览器直接卡顿Node里跑批处理也会被明显拖慢。我拿一万次循环做过对比带console.log的版本比不带慢了几十倍印象极深。高频场景尤其要注意mousemove、scroll、requestAnimationFrame、WebSocket消息回调这些事件本来就是高频触发你要是每次回调都console.log一条控制台会刷到完全没法看还会拖垮页面性能。我的做法是这类高频日志开一个开关平时关闭需要排查时打开并加节流比如只在状态变化时打印或者每N次才打一条。调试期可以随便打但代码提交前要检查这些高频日志该删的删、该降级的降级。5. 生产级日志治理级别开关、样式化、脱敏与快照陷阱5.1 %c样式化把控制台变成可视化面板前面表格里提到%c现在展开聊。%c可以给后续文本套上CSS样式让日志从普通文本变成带背景色、文字色、圆角、内边距的“标签块”。console.log(%c[API]%c 请求失败, background:#f44336;color:#fff;padding:2px 6px;border-radius:3px, color:#f44336)多个%c可以给一段日志的不同片段分别设样式。这个技巧做服务启动banner很合适比如打印项目名和版本号给请求日志加一个醒目的彩色标签也很实用扫控制台时一眼就能区分请求、响应、警告和错误。注意Node环境不认%c样式化是浏览器专属玩法。另外样式别用得太花它的价值是让重要信息跳出来而不是把整个控制台变成调色盘。5.2 一个可复用的分级日志封装除了原生的console方法我更推荐在项目里做一个极简的Logger封装按debug/info/warn/error分级通过环境变量或配置控制输出级别。这样线上可以把debug和info关掉保留warn和error日志量立刻降下来同时代码里已经写好的日志不用删。const LEVELS { debug: 10, info: 20, warn: 30, error: 40 } class Logger { constructor(options {}) { this.level options.level || debug this.timestamps options.timestamps ! false } shouldLog(level) { return LEVELS[level] LEVELS[this.level] } format(level, args) { const parts [] if (this.timestamps) parts.push(new Date().toISOString()) parts.push([${level.toUpperCase()}]) return parts.concat(args) } debug(...args) { if (!this.shouldLog(debug)) return console.log(...this.format(debug, args)) } info(...args) { if (!this.shouldLog(info)) return console.info(...this.format(info, args)) } warn(...args) { if (!this.shouldLog(warn)) return console.warn(...this.format(warn, args)) } error(...args) { if (!this.shouldLog(error)) return console.error(...this.format(error, args)) } } const logger new Logger({ level: process.env.LOG_LEVEL || debug }) logger.info(server started on port %d, 8080)这套封装只有几十行但已经覆盖了80%的日志治理需求。想再进一步还可以在format里通过new Error().stack截取调用位置把文件名和行号拼进日志方便定位是哪个模块打出来的。调试期可以开启线上如果担心性能开销再关掉。5.3 生产环境日志开关、例外与脱敏思路生产环境怎么处理日志网上说法很多我试过几种最后沉淀下来的原则是别指望构建工具自动删除所有console.log。有些方案会连console.error一起删掉线上犯错时什么都没有。更稳妥的做法是入口层控制级别生产环境把Logger的level设为warn业务日志全关错误日志保留。如果确实想靠构建期清理新一代Webpack的terser-webpack-plugin里更可控的做法是配置terserOptions.compress.pure_funcs把console.log、console.info、console.debug声明为纯函数让压缩阶段把它们剔除而console.warn和console.error保留。无论用哪种方案都要在构建产物里实际跑一次验证别只看配置生效就以为万事大吉。脱敏也必须提。很多项目直接在业务代码里console.log(token、手机号、身份证号)这些信息会原样进浏览器控制台、进日志文件、进日志收集系统安全隐患非常大。日志封装里可以加一层脱敏处理匹配常见的敏感字段名和手机号输出前替换成***。调试阶段可以临时关闭脱敏方便定位但线上输出必须默认开启。5.4 打印引用对象时的“实时快照”陷阱以及我的收尾心得最后说一说一个非常容易误导人的细节浏览器控制台里console.log一个对象之后如果你在后续代码里修改了这个对象的属性再回到控制台展开之前那条日志看到的很可能不是打印时刻的值而是当前时刻的值。原因是DevTools对对象采用惰性求值展开时才去读取对象的当前状态。const user { name: Alice } console.log(before, user) user.name Bob // 浏览器里展开上面那条 before 日志name 很可能显示为 Bob这个现象会让新手怀疑人生以为代码执行顺序都错了。解决起来也简单打印时做一个浅拷贝console.log(before, { ...user })或者直接输出JSON字符串。Node环境打印对象时输出的是格式化快照一般不受这个影响保险起见也可以序列化后再看。调试这事最终拼的还是习惯。我现在的做法是调试期大胆打凡是能帮助定位的地方都拆开打代码合入前过一遍日志能合并的合并能降级的降级线上环境只留warn和error并保证日志里不出现手机号、token这类敏感信息。说到底console.log学再多技巧也不如建立一套自己的日志规范。如果你也想让自己的调试过程更顺手建议从把今天讲到的API各用一遍开始用熟了再谈怎么治理。
返回列表