在浏览器里 hook setInterval:原理和五种写法

在浏览器里”hook” 某个内置函数,本质就是——把原函数偷出来,用你自己的函数替换它setInterval 是最常见的 hook 目标:调试疯狂轮询、监控页面定时器、干掉反调试轮询。

一句话原理

const _origin = window.setInterval;

window.setInterval = function (fn, delay, ...args) {
    // 你的自定义逻辑
    return _origin(fn, delay, ...args);   // 回到原函数
};

_origin 是原函数引用,window.setInterval 是你替换后的新函数。之后所有 setInterval(...) 调用都会先经过你。

场景一:监控所有调用

(function () {
    const _origin = window.setInterval;
    window.setInterval = function (fn, delay, ...args) {
        console.log("setInterval:", { delay, fn: fn.toString().slice(0, 80) });
        return _origin(fn, delay, ...args);
    };
})();

用途:找出页面里到底谁在挂定时器。

场景二:按条件拦截

只关心 1 秒轮询:

window.setInterval = function (fn, delay, ...args) {
    if (delay === 1000) {
        console.trace("1s interval:", fn);
    }
    return _origin(fn, delay, ...args);
};

console.trace 直接打出调用栈,能追到是哪段代码挂的。

场景三:阻止高频轮询

页面每 50ms 疯狂检查某个东西(典型反调试或广告轮询),CPU 直接吃满:

window.setInterval = function (fn, delay, ...args) {
    if (delay < 500) {
        console.log("阻止高频 interval:", delay);
        return null;    // 直接返回 null,不真的开定时器
    }
    return _origin(fn, delay, ...args);
};

场景四:记录所有 ID 方便统一清

(function () {
    const _origin = window.setInterval;
    const ids = new Set();

    window.setInterval = function (fn, delay, ...args) {
        const id = _origin(fn, delay, ...args);
        ids.add(id);
        return id;
    };

    window.__clearAll = () => {
        ids.forEach(clearInterval);
        ids.clear();
        console.log("全部清除");
    };
})();

在 devtools 里手动 __clearAll() 就能一键把当前页面所有定时器停了。

场景五:油猴脚本里 hook

必须 @run-at document-start——否则页面脚本先跑,等你 hook 上去已经晚了:

// ==UserScript==
// @name         hook setInterval
// @match        https://example.com/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
    const _origin = window.setInterval;
    window.setInterval = function (fn, delay, ...args) {
        console.log("intercepted:", delay);
        return _origin.call(window, fn, delay, ...args);
    };
})();

注意用 _origin.call(window, ...)——某些浏览器的 setInterval 严格要求 this === window,不 bind 会抛 Illegal invocation

常见反 hook 手法

页面可能这样绕过你:

// 提前保存原始引用
const _real = window.setInterval.bind(window);
_real(fn, 100);
  • 页面代码保存的是你的 hook 前的原函数,你就拦不到
  • 或者 iframe 里跑,window 隔离
  • 或者用 postMessage + Worker,压根不走主线程 setInterval

对策:

  • @run-at document-start 越早越好
  • 同时 hook setTimeout / requestAnimationFrame——很多站点会混着用
  • iframe 里的定时器要另外注入

用 Proxy 更隐蔽

上面的写法暴露痕迹(window.setInterval.toString() 会变),要更隐蔽:

window.setInterval = new Proxy(window.setInterval, {
    apply(target, thisArg, args) {
        const [fn, delay] = args;
        console.log("intercepted:", delay);
        return Reflect.apply(target, thisArg, args);
    },
});

Proxy 版本的 .toString() 还是原始的,页面用 toString().includes("[native code]") 检测更难发现。

一句话总结

hook = 存原函数引用 + 替换 window 属性。油猴里必须 run-at document-start,进阶用 Proxy 更隐蔽。同一套模板套 setTimeout / fetch / XMLHttpRequest 也照样能用。