
在单页应用中,为监听 `pushstate` 导致的 url 变更,可通过代理 `window.history.pushstate` 方法实现;但直接调用原函数会因上下文丢失引发 “illegal invocation” 错误,需使用 `call()` 显式绑定 `this` 到 `window.history`。
在现代前端开发中,许多第三方库需要感知路由变化(如埋点、状态同步、A/B 测试等),但标准 window.history.pushState 不会触发任何 DOM 事件,且无法被外部库直接拦截。一种常见且轻量的解决方案是方法劫持(method patching):保存原始方法,再将其替换为自定义包装函数,在执行原逻辑前/后注入事件通知。
然而,直接解构赋值并调用会导致 TypeError: Illegal invocation —— 这是因为 pushState 是一个强绑定于 history 对象的宿主方法,其内部依赖 this 指向 window.history 才能正确访问浏览器历史栈。一旦脱离上下文(例如 const fn = history.pushState; fn(...)),就会抛出非法调用错误。
✅ 正确做法是使用 .call() 或 .apply() 显式指定 this:
// ✅ 安全劫持:保留原始上下文
const originalPushState = window.history.pushState;
window.history.pushState = function(state, title, url) {
// 发送自定义事件,供你的库监听
const navEvent = new CustomEvent('route-change', {
detail: { state, url, type: 'push' }
});
window.dispatchEvent(navEvent);
// 关键:必须绑定 this 到 window.history
return originalPushState.call(window.history, state, title, url);
};⚠️ 注意事项:
- 不要使用箭头函数重写:箭头函数无法通过 call()/apply() 改变 this,且本身不绑定 this,会导致上下文丢失;
- 兼容 replaceState:若需同时监听替换操作,应同步劫持 window.history.replaceState,逻辑一致;
- 避免重复劫持:建议添加守卫逻辑(如检查 window.history.pushState._patched 标志),防止多次 patch 导致事件重复触发;
- 考虑 popstate 补充:pushState/replaceState 不会触发 popstate,但用户点击浏览器前进/后退时会触发;你的库应同时监听 popstate 与自定义事件,确保覆盖所有导航场景。
? 进阶建议:
若项目已使用 History API 封装库(如 history 包或 react-router 的 navigate),优先通过其提供的 listen API 接入,而非直接 patch 原生 API——更健壮、可测试、易维护。仅在无侵入式接入能力时,才采用上述劫持方案。
至此,你的外部库即可通过 window.addEventListener('route-change', handler) 稳定捕获所有 pushState 触发的导航行为,实现解耦、可靠的状态响应。










