做前端逆向、爬虫开发或者线上问题排查时,很多人都遇到过这种场景:刚按下 F12 打开开发者工具,页面就立刻卡在 debugger 断点上,点一下继续执行,马上又断住,循环往复根本没法正常调试代码。这就是前端最常见的反调试手段 ——无限 debugger。
它的原理并不复杂,本质是利用浏览器开发者工具开启时会执行 debugger 语句并中断执行的特性,通过循环、递归等方式持续触发断点,从而抬高代码分析的门槛。本文从原理、常见实现到不同层级的破解方案,完整拆解无限 debugger 的应对思路。
一、无限 debugger 的核心逻辑与常见实现
所有无限 debugger 的底层逻辑都一致:在检测到开发者工具打开的状态下,以极低的间隔持续触发 debugger 语句。根据实现方式的隐蔽程度,常见的有 5 种类型:
setInterval(function () {
debugger;
}, 100);
特点是逻辑简单、容易定位,但也最容易被清除。
setInterval(function() {
Function('debugger')();
}, 100);
代码里不会直接出现 debugger 字面量,普通的字符串过滤会失效。
(function debugLoop() {
debugger;
requestAnimationFrame(debugLoop);
})();
二、入门级破解:浏览器原生功能快速绕过
面对普通的无限 debugger,用浏览器自带的功能就能快速解决,适合临时快速调试的场景。
1. 一键禁用所有断点
这是最简单粗暴的方法:打开开发者工具的「Sources」面板,点击顶部的「停用断点」图标(或直接按快捷键 Ctrl + F8 / Cmd + F8)。 开启后所有断点包括 debugger 都会失效,页面可以正常运行。
- 优点:操作零成本,一秒生效
- 缺点:自己也无法设置断点调试,适合只看页面运行逻辑、不需要断点分析的场景
2. 单点位永久跳过:Never pause here
当 debugger 第一次断住时,在调用栈中找到触发 debugger 的那一行代码,右键点击左侧行号,选择「Never pause here」(永不中断)。 浏览器会自动给这一行添加一个条件为 false 的条件断点,之后执行到这里永远不会断住。
- 优点:不影响自己设置其他断点,针对性强
- 缺点:只对固定行号的代码有效,如果是动态生成的 VM 脚本、多位置触发的 debugger,需要逐个添加
三、进阶级破解:Hook 原生 API 从源头拦截
面对动态生成、多点位触发的无限 debugger,更高效的方式是在代码执行前,劫持浏览器原生 API,从源头拦截 debugger 的生成。
1. 暴力清除所有定时器
绝大多数无限 debugger 都依赖定时器驱动,批量清除所有定时器 ID 就能直接终止循环:
// 暴力清除 1~9999 范围内的所有定时器
for (let i = 1; i < 9999; i++) {
clearInterval(i);
clearTimeout(i);
}
注意:这种方法会把页面正常业务逻辑的定时器也一起清除,可能导致页面功能异常,仅适合纯调试逻辑、不关心页面正常运行的场景。
2. 劫持定时器,过滤反调试回调
更优雅的方式是重写 setInterval / setTimeout,在定时器注册时就过滤掉包含 debugger 的回调,不影响正常业务定时器:
// 保存原生方法
const originalSetInterval = window.setInterval;
window.setInterval = function (callback, delay) {
// 字符串类型回调
if (typeof callback === 'string' && callback.includes('debugger')) {
return -1; // 返回无效ID,不注册
}
// 函数类型回调,转字符串检测
if (typeof callback === 'function') {
if (callback.toString().includes('debugger')) {
return -1;
}
}
return originalSetInterval.apply(this, arguments);
};
3. 拦截动态执行:Hook Function 与 eval
针对用 Function('debugger')()、eval 动态执行的反调试代码,可以重写构造函数和 eval 方法:
// Hook Function 构造器
const originalFunction = Function;
Function = function (…args) {
const code = args.join(' ');
if (code.includes('debugger')) {
// 遇到包含 debugger 的代码,返回空函数
return function () {};
}
return originalFunction.apply(this, args);
};
// Hook eval
const originalEval = window.eval;
window.eval = function (code) {
if (code.includes('debugger')) return;
return originalEval.call(this, code);
};
这种方法能拦截绝大多数动态生成的 debugger 代码,对 VM 匿名脚本也有效。
四、高阶破解:应对混淆加密的强反调试
当网站使用代码混淆、动态加密、多文件分散反调试逻辑时,上面的 Hook 方法可能被绕过,这时需要更底层的解决方案。
1. 本地文件替换(Local Overrides)
这是通用性最强、一劳永逸的方案,利用浏览器的「本地覆盖」功能,用修改后的本地文件替换线上的 JS 文件。 操作步骤:
这种方法不受代码混淆、加密的影响,只要能定位到源文件就能破解,适合需要长期调试的站点。
2. 油猴脚本前置注入
如果反调试代码执行时机很早,手动 Hook 来不及,可以用油猴(Tampermonkey)脚本在页面加载前就注入 Hook 代码。 脚本设置 @run-at document-start,确保在页面任何 JS 执行之前就完成原生 API 的劫持,从最开始就拦截所有 debugger 逻辑。
3. 条件断点过滤调用栈
对于分散在多处、找不到源头的 debugger,可以在 debugger 断住时添加条件断点,通过调用栈特征跳过所有反调试断点:
// 条件断点内容:调用栈中不包含反调试函数特征时才中断
!new Error().stack.includes('反调试函数名特征')
适合已经定位到反调试函数名,但不方便修改源码的场景。
五、总结
无限 debugger 是前端反调试体系里最基础的手段,所有破解思路本质上都归为三类:跳过执行、拦截生成、替换源码。
- 临时快速调试:用禁用断点、Never pause here 即可
- 中等强度反调试:用 Hook 原生 API 从源头拦截
- 高强度混淆站点:优先使用本地文件替换方案
反调试和调试永远是成本对抗,没有绝对无法破解的防护,也没有万能的破解方法,根据站点的防护强度选择对应的方案,才是最高效的做法。






