在浏览器中,每次页面跳转、每次 location 变化,都会被记录在会话历史(session history)堆栈中。window.history 对象提供了对这一堆栈的编程访问能力,它既能让开发者读取历史记录信息,也能通过 API 控制浏览器的前进后退,甚至在不刷新页面的情况下修改 URL——这正是现代前端路由实现的基础。
14.4.1 基本属性
// 当前历史堆栈中的记录总数(只读)
console.log(history.length);
// 当前历史记录的状态对象(通过 pushState/replaceState 设置的)
console.log(history.state);
length表示当前标签页在同一个浏览器会话中累积的历史记录数量。用户打开新标签页后,length为 1(当前页)。state是与当前历史记录相关联的 JavaScript 对象,由pushState()或replaceState()传入。如果没有通过这两个方法设置过状态,则为null。
这两个属性在日常开发中更多用于调试和边缘判断,实际导航主要依赖方法。
14.4.2 导航方法:go / back / forward
“前进后退”是用户最熟悉的浏览器操作,history 对象提供了对应的编程调用:
// 后退一页,等价于 history.go(-1)
history.back();
// 前进一页,等价于 history.go(1)
history.forward();
// 跳转到历史堆栈中的任意位置
history.go(-2); // 后退两页
history.go(0); // 刷新当前页(相当于 location.reload())
当你调用 back() 或 forward() 时,浏览器会从会话历史堆栈中加载对应的记录并导航到该页面——注意,这可能会导致页面完全刷新,除非目标记录是由 pushState() / replaceState() 创建的(详见下一节)。
14.4.3 状态管理 API:pushState 与 replaceState
这是 history 对象中最重要、也最常被现代 Web 应用使用的两个方法。它们允许你在不刷新页面的前提下修改地址栏的 URL,并可以在历史记录中保存任意 JavaScript 对象作为“状态”。
pushState(state, title, url)
// 向历史堆栈压入一条新记录
history.pushState(
{ page: 1, from: 'list' }, // 状态对象
'', // 标题(目前浏览器大多忽略此参数)
'/page/1' // 新 URL(相对或绝对,必须同源)
);
执行 pushState 后,浏览器地址栏会显示 /page/1,但页面本身不会重新加载,也不会对 URL 发起请求。历史堆栈中多了一条记录,用户点击后退按钮会触发 popstate 事件(而不是直接回退到上一个页面)。
replaceState(state, title, url)
// 替换当前历史记录,不增加新记录
history.replaceState(
{ page: 2 }, // 新状态对象
'',
'/page/2'
);
replaceState 与 pushState 的区别在于它不产生新的历史记录,而是直接修改当前记录的状态和 URL。这在实现“重定向但不想污染后退堆栈”的场景中很有用,例如登录后跳转到首页并替换掉登录页的历史记录。
几点重要限制与注意事项:
- 两个方法都只能修改同源的 URL,不能跨域修改,否则会抛出
DOMException。 - 修改 URL 时,如果仅变化 hash(
#部分),浏览器不会真正导航,这与直接修改location.hash效果类似,但可以附带状态对象。 - 修改路径部分(如
/page/1到/page/2)时,浏览器不会加载新页面,但如果用户此时手动刷新,浏览器会按新的 URL 发起请求——这要求你的 Web 服务器能够正确处理这些“虚拟”路径(通常是配置重写规则,将请求指向入口 HTML 文件)。 - 状态对象可以是任何可序列化的 JavaScript 值(通过结构化克隆算法保存),但应尽量控制体积,因为状态会保留在内存中直到历史记录被清除。
14.4.4 popstate 事件
当用户点击浏览器的前进、后退按钮,或者 JavaScript 调用 history.back() / go() 时,浏览器会触发 window 对象上的 popstate 事件。注意:pushState 和 replaceState 不会触发 popstate。
window.addEventListener('popstate', function(event) {
// event.state 就是当初 pushState/replaceState 保存的状态对象
console.log('location changed to:', document.location.href);
console.log('state:', event.state);
});
popstate 事件就绪后,我们可以根据 event.state 中的信息来更新页面内容,实现“无刷新路由切换”。这是所有前端路由库(React Router、Vue Router 等)在 history 模式下的底层原理。
14.4.5 前端路由的两种实现方式
结合 history 与 location 对象的特性,前端路由在实践中分化出两种主流方案:
1. Hash 模式
利用 location.hash 和 hashchange 事件。URL 中 # 及其后面的部分不会发送到服务器,天然支持无刷新切换,兼容性极好,服务器无需做任何配置。例如 #/page/1。
window.addEventListener('hashchange', function() {
const route = location.hash.slice(1); // '/page/1'
// 根据 route 渲染对应组件
});
2. History 模式
利用 pushState / replaceState + popstate 事件,URL 完全干净(/page/1),无 # 符号,更符合传统网站的审美和 SEO 要求。但需要服务端配合:在找不到对应静态文件时,将所有请求重定向到入口 HTML(即“兜底规则”),否则用户直接访问 /page/1 或刷新页面会得到 404。
// 简易路由类示意
class Router {
constructor() {
this.routes = {};
window.addEventListener('popstate', () => this._render());
}
navigate(path) {
history.pushState({}, '', path);
this._render();
}
_render() {
const path = location.pathname;
// 根据 path 查找并更新页面内容
}
}
14.4.6 实际开发实践
在真实项目中,你几乎不会直接操作 history 对象来实现路由,而是使用成熟的客户端路由库:
- React Router 使用
<BrowserRouter>(history 模式)或<HashRouter>(hash 模式),内部封装了对pushState/replaceState和popstate的监听。 - Vue Router 提供
mode: 'history'和mode: 'hash'选项,行为与上述描述完全一致。 - Next.js / Nuxt.js 等框架在文件系统路由的基础上,进一步封装了 history 操作,开发者几乎无感。
然而,理解这些封装背后的原生 API 非常必要:当需要实现自定义路由器、在非框架项目中处理简单的“页面切换”,或者在面试中解释前端路由原理时,history 对象的知识才是真正的底气。
14.4.7 小结
history.back()/forward()/go()控制浏览器的物理前进后退,用于传统导航。pushState()/replaceState()让开发者在不刷新页面的情况下修改 URL 并携带状态对象,是 SPA 路由的核心。popstate事件在历史记录发生跳转时触发,用于响应浏览器导航操作。- 实际开发中推荐使用成熟的路由库,但掌握原生能力能让你从容应对定制化需求和底层排错。
理解 history 对象的运作机制,你就真正掌握了现代 Web 应用中“无刷新页面切换”的精髓。接下来,我们将进入浏览器中另一个非常重要的定时与动画控制——14.5 节定时器和 requestAnimationFrame。