{T}

history 对象

概述

history 对象是浏览器对象模型(BOM)的核心组成部分,表示当前窗口自创建以来的导航历史记录栈。它提供了一组属性和方法,允许脚本在不直接暴露完整 URL 列表的前提下,控制浏览器的前进与后退行为。

在浏览器架构中的位置

code
┌─────────────────────────────────────────────────────────┐
│                        window                            │
│  ┌────────────────────────────────────────────────────┐ │
│  │                    BOM 对象                         │ │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐        │ │
│  │  │ location │  │ history  │  │ navigator│        │ │
│  │  └──────────┘  └──────────┘  └──────────┘        │ │
│  │                                                  │ │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐        │ │
│  │  │  screen  │  │ document │  │localStorage│      │ │
│  │  └──────────┘  └──────────┘  └──────────┘        │ │
│  └────────────────────────────────────────────────────┘ │
│                        ↓                                │
│              浏览器历史记录栈                            │
│  ┌────┬────┬────┬────┬────┬────┬────┐               │
│  │Page│Page│Page│Page│Page│Page│Page│               │
│  │ 1  │ 2  │ 3  │ 4  │ 5  │ 6  │ ←当前             │
│  └────┴────┴────┴────┴────┴────┴────┘               │
└─────────────────────────────────────────────────────────┘

核心特性

  • 会话级作用域history 挂载在 window 上,不同的浏览上下文(标签页、iframe)各自维护独立的历史栈
  • 同源策略限制:只能访问与当前页面同源的历史条目,跨源页面的 URL 与详细信息对脚本不可见
  • 状态持久化:历史记录中的每个条目都会保存页面状态,便于在前进/后退时恢复 UI
  • 无刷新导航:配合 pushState()replaceState() 可实现 URL 变化而不刷新页面

与其他 BOM 对象的关系

对象关系说明协作场景
locationhistory 管理导航栈,location 提供当前 URL 信息URL 变化时两者同步更新;location.hash 变化会创建历史条目
windowhistory 的宿主对象,提供 popstatehashchange 事件监听导航事件,处理页面状态恢复
document历史条目关联文档状态页面状态恢复时更新 DOM

ℹ️ - history 只能访问与当前页面同源的历史条目,跨源页面的 URL 与详细信息对脚本不可见 - 历史记录中的每个条目都会保存页面状态,便于在前进/后退时恢复 UI

主要属性

length

history.length 表示历史栈中的条目数量,包括当前页面在内:

javascript
console.log(history.length)

可以用它来判断用户是否第一次打开页面:

javascript
if (history.length === 1) {
  console.log("用户可能是首次访问该标签页")
}

// 实际应用:决定返回按钮的行为
function goBack() {
  if (history.length > 1) {
    history.back()
  } else {
    // 没有历史记录,跳转到首页
    location.href = "/"
  }
}

state

history.state 返回当前历史条目的状态对象,这与最近一次 pushState()replaceState() 传入的对象一致:

javascript
const currentState = history.state
if (currentState) {
  restoreUI(currentState)
}

该对象会在 popstate 事件中作为 event.state 再次传回。

状态对象的限制

javascript
// ✅ 正确:可序列化的对象
history.pushState({ id: 123, name: "test" }, "", "/page/123")

// ❌ 错误:包含函数或循环引用
history.pushState({ callback: () => {} }, "", "/page") // 函数会被忽略

// ❌ 错误:过大的对象(浏览器限制约 500KB - 2MB)
const largeState = { data: new Array(1000000).fill("x") }
history.pushState(largeState, "", "/page") // 可能抛出错误

scrollRestoration

history.scrollRestoration 控制浏览器在导航时的滚动行为:

  • auto(默认):浏览器在返回页面时自动恢复滚动位置
  • manual:禁用自动恢复,需要脚本自行处理
javascript
// 禁用自动滚动恢复
history.scrollRestoration = "manual"

// 手动处理滚动
window.addEventListener("popstate", () => {
  const state = history.state
  if (state && state.scrollTop) {
    window.scrollTo({ top: state.scrollTop, behavior: "smooth" })
  }
})

// 保存滚动位置到状态
function saveScrollState() {
  const state = {
    ...history.state,
    scrollTop: window.scrollY
  }
  history.replaceState(state, "", location.href)
}

导航方法

back() 与 forward()

history.back()history.forward() 分别等价于用户点击浏览器的"后退"和"前进"按钮:

javascript
history.back() // 后退一步
history.forward() // 前进一步

调用它们不会触发页面刷新,而是从历史栈中加载缓存的条目。

实际应用示例

html
<!-- 自定义返回按钮 -->
<button onclick="handleBack()">返回上一页</button>

<script>
  function handleBack() {
    // 检查是否有历史记录
    if (history.length > 1) {
      history.back()
    } else {
      // 没有历史记录,跳转到首页
      location.href = "/"
    }
  }
</script>

go()

history.go() 支持按步数向前或向后导航:

  • 负数:向后退,例如 history.go(-1) 等价于 history.back()
  • 正数:向前进,例如 history.go(2) 向前跳两步
  • 0:重新加载当前页面
javascript
history.go(-1) // 后退一页
history.go(1) // 前进一页
history.go(0) // 刷新当前页面
history.go(-3) // 后退三页

部分浏览器允许传入字符串尝试匹配历史条目,但兼容性较差,生产环境不推荐使用。

安全导航示例

javascript
function safeGo(steps) {
  try {
    // 检查是否可以安全导航
    if (steps < 0 && Math.abs(steps) >= history.length) {
      console.warn("历史记录不足,无法后退指定步数")
      return false
    }

    history.go(steps)
    return true
  } catch (error) {
    console.error("导航失败:", error)
    return false
  }
}

URL 与散列(hash)变化

修改 location 的不同部分会对历史栈产生不同影响:

  • 修改非 hash 部分(如 pathnamesearch)会创建新历史条目并触发页面加载
  • 修改 hash 会创建新历史条目,但不会导致页面刷新
javascript
location.hash = "section-2" // 添加一条只改变散列的历史记录

这也是早期单页应用程序(SPA)通过监听 hashchange 事件实现前进/后退控制的方式。

HTML5 历史状态管理

现代浏览器提供 pushState()replaceState() 方法,使得 SPA 可以在不刷新页面的情况下更改地址栏并维护状态。

历史栈工作原理

code
用户操作              历史栈状态                URL 显示
─────────────────────────────────────────────────────────
初始访问页面 A         [A]                      /page-a
                    ↓
点击链接到页面 B       [A, B]                   /page-b
                    ↓
执行 pushState(C)     [A, B, C]                /page-c
                    ↓
点击后退按钮          [A, B] ←当前             /page-b
                    ↓
执行 replaceState(D)  [A, D]                   /page-d
                    ↓
点击前进按钮          (无法前进,D已替换B)

pushState()

history.pushState(state, title, url?) 会向历史栈压入一个新条目,并更新地址栏:

javascript
const state = { tab: "pricing" }
history.pushState(state, "Pricing", "/pricing")
renderPricing()

参数说明:

参数类型说明
stateany与当前 UI 状态相关的序列化数据对象(最大约 500 KB ~ 1 MB)
titlestring历史条目的标题,当前主流浏览器会忽略,可传空字符串
urlstring?与当前源同源的相对或绝对地址

调用后不会立即向服务器发起请求,URL 的变化仅体现在地址栏与历史栈中。

完整示例

javascript
// 标签页切换
function switchTab(tabName) {
  const state = { tab: tabName, timestamp: Date.now() }
  const url = `/dashboard/${tabName}`

  // 更新历史记录和 URL
  history.pushState(state, "", url)

  // 更新页面内容
  renderTab(tabName)

  // 更新导航高亮
  updateNavigation(tabName)
}

// 绑定事件
document.querySelectorAll(".tab-button").forEach((btn) => {
  btn.addEventListener("click", () => {
    switchTab(btn.dataset.tab)
  })
})

replaceState()

history.replaceState(state, title, url?) 会用新的状态替换当前条目,而不会新增历史记录:

javascript
history.replaceState({ tab: "overview" }, "Overview", "/overview")

适用于用户尚未确认操作或需要修正当前 URL 的场景。

实际应用场景

javascript
// 场景1:表单步骤导航(不希望用户返回到中间步骤)
function nextStep(currentStep) {
  const nextStepNum = currentStep + 1
  const state = { step: nextStepNum }
  const url = `/form/step/${nextStepNum}`

  // 使用 replaceState,防止用户返回中间步骤
  if (currentStep === 0) {
    history.replaceState(state, "", url)
  } else {
    history.pushState(state, "", url)
  }

  // ... 中间省略 ...

  const searchParams = new URLSearchParams(filters)
  const url = `${location.pathname}?${searchParams}`

  history.replaceState(state, "", url)
  applyFilters(filters)
}

popstate 事件

当用户通过前进或后退导航到一个由 pushState()replaceState() 创建的历史条目时,会触发 popstate 事件:

javascript
window.addEventListener("popstate", (event) => {
  const state = event.state
  if (state) {
    renderByState(state)
  } else {
    renderInitialView()
  }
})

首次加载页面时不会触发 popstate。此外,通过脚本调用 pushState()replaceState() 也不会触发该事件,只有用户导航行为才会触发。

事件触发流程

code
用户点击后退/前进按钮
         ↓
浏览器从历史栈加载条目
         ↓
URL 更新到对应地址
         ↓
触发 popstate 事件
         ↓
事件处理器接收 state 对象
         ↓
恢复页面状态

浏览器兼容性

API 支持情况

APIChromeFirefoxSafariEdgeIE
history.length
history.back()
history.forward()
history.go()
history.pushState()5+4+5+12+10+
history.replaceState()5+4+5+12+10+
history.state5+4+6+12+10+
history.scrollRestoration46+46+11+79+

兼容性处理

javascript
// 检测 API 支持
function isHistoryApiSupported() {
  return !!(window.history && history.pushState)
}

// 降级处理
function safePushState(state, title, url) {
  if (isHistoryApiSupported()) {
    history.pushState(state, title, url)
  } else {
    // 降级到 hash 路由
    location.hash = url
  }
}

// 检测滚动恢复支持
function setScrollRestoration(mode) {
  if ('scrollRestoration' in history) {
    history.scrollRestoration = mode
  } else {
    console.warn('当前浏览器不支持 scrollRestoration API')
  }
}

安全性考虑

同源策略限制

history API 受到严格的同源策略保护:

javascript
// ✅ 正确:同源 URL
history.pushState({}, "", "/new-page")

// ❌ 错误:跨域 URL 会抛出异常
history.pushState({}, "", "https://other-domain.com/page")
// DOMException: Failed to execute 'pushState' on 'History': 
// A history state object with URL 'https://other-domain.com/page' 
// cannot be created in a document with origin 'https://example.com'

安全最佳实践

1. 验证 URL 格式

javascript
function safePushState(state, url) {
  try {
    // 验证 URL 是否为同源
    const absoluteUrl = new URL(url, location.origin)
    if (absoluteUrl.origin !== location.origin) {
      throw new Error('跨域 URL 不被允许')
    }

    history.pushState(state, '', url)
    return true
  } catch (error) {
    console.error('pushState 失败:', error)
    return false
  }
}

2. 防止状态对象注入攻击

javascript
// ❌ 危险:直接使用用户输入
const userInput = getUserInput()
history.pushState({ content: userInput }, '', '/page')

// ✅ 安全:过滤敏感数据
function sanitizeState(state) {
  const allowedKeys = ['id', 'name', 'page']
  const sanitized = {}
  
  for (const key of allowedKeys) {
    if (state[key] !== undefined) {
      sanitized[key] = state[key]
    }
  }
  
  return sanitized
}

const safeState = sanitizeState(userInput)
history.pushState(safeState, '', '/page')

3. 防止历史记录劫持

javascript
// 限制历史记录条目数量
const MAX_HISTORY_ENTRIES = 50
let historyCount = 0

function controlledPushState(state, url) {
  if (historyCount >= MAX_HISTORY_ENTRIES) {
    // 替换最早的条目
    history.go(-(MAX_HISTORY_ENTRIES - 1))
    history.replaceState(state, '', url)
  } else {
    history.pushState(state, '', url)
    historyCount++
  }
}

4. 敏感信息保护

javascript
// ❌ 错误:存储敏感信息
history.pushState({ 
  userId: 123,
  token: 'sensitive-token' // 不要在状态中存储敏感数据
}, '', '/dashboard')

// ✅ 正确:只存储必要的非敏感信息
history.pushState({ 
  userId: 123,
  page: 'dashboard'
}, '', '/dashboard')

// 或者使用加密存储
function encryptState(state) {
  // 实现加密逻辑
  return btoa(JSON.stringify(state))
}

function decryptState(encrypted) {
  return JSON.parse(atob(encrypted))
}

性能优化

状态对象大小优化

javascript
// 检查状态对象大小
function getStateSize(state) {
  try {
    return new Blob([JSON.stringify(state)]).size
  } catch (error) {
    console.error('无法计算状态大小:', error)
    return Infinity
  }
}

// 优化状态对象
function optimizeState(state, maxSize = 500000) {

  // ... 中间省略 ...

  filters: { status: 'active' },
  largeData: hugeArray // 大型数据
}

const optimizedState = optimizeState(state)
history.pushState(optimizedState, '', '/list')

批量更新优化

javascript
// 防抖处理频繁的状态更新
function createDebouncedPushState(delay = 300) {
  let timeoutId = null
  let lastState = null
  let lastUrl = null
  
  return function debouncedPushState(state, url) {
    lastState = state
    lastUrl = url
    
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      history.pushState(lastState, '', lastUrl)
    }, delay)
  }
}

const debouncedPushState = createDebouncedPushState()

// 在滚动或筛选变化时使用
window.addEventListener('scroll', () => {
  const state = { scrollY: window.scrollY }
  debouncedPushState(state, location.href)
})

内存管理

javascript
// 历史记录管理器
class HistoryManager {
  constructor(maxEntries = 50) {
    this.maxEntries = maxEntries
    this.currentEntries = 0
  }
  
  push(state, url) {
    // 检查并清理过多的历史记录
    if (this.currentEntries >= this.maxEntries) {
      this.cleanup()
    }

  // ... 中间省略 ...

    })
  }
}

const historyManager = new HistoryManager()
historyManager.trackPopstate()

实际应用场景

场景1:无限滚动分页

javascript
class InfiniteScrollWithHistory {
  constructor() {
    this.currentPage = 1
    this.isLoading = false
    this.init()
  }
  
  init() {
    // 监听滚动事件
    window.addEventListener('scroll', this.handleScroll.bind(this))
    
    // 监听历史记录变化

  // ... 中间省略 ...

    // 恢复滚动位置
    if (state.scrollY) {
      window.scrollTo(0, state.scrollY)
    }
  }
}

场景2:多标签页状态管理

javascript
class TabManager {
  constructor() {
    this.tabs = new Map()
    this.currentTab = null
    this.init()
  }
  
  init() {
    // 拦截标签点击
    document.querySelectorAll('[data-tab]').forEach(tab => {
      tab.addEventListener('click', (e) => {
        e.preventDefault()

  // ... 中间省略 ...

      const url = `#tab-${tabId}`
      
      history.pushState(state, '', url)
    }
  }
}

场景3:表单向导

javascript
class FormWizard {
  constructor(steps) {
    this.steps = steps
    this.currentStep = 0
    this.formData = {}
    this.init()
  }
  
  init() {
    // 监听历史记录
    window.addEventListener('popstate', (e) => {
      if (e.state && typeof e.state.step === 'number') {

  // ... 中间省略 ...

  async submit() {
    // 提交表单
    console.log('提交数据:', this.formData)
    // ... 提交逻辑
  }
}

实际应用:单页应用路由

简易路由实现

javascript
class SimpleRouter {
  constructor() {
    this.routes = new Map()
    this.init()
  }

  init() {
    // 监听 popstate 事件
    window.addEventListener("popstate", (event) => {
      this.handleRoute(event.state)
    })


  // ... 中间省略 ...

  .route("*", () => {
    render404Page()
  })

// 导航
router.navigate("/about")

完整的路由管理器

javascript
class Router {
  constructor(options = {}) {
    this.routes = []
    this.currentRoute = null
    this.beforeHooks = []
    this.afterHooks = []
    this.options = {
      scrollBehavior: "auto",
      ...options
    }

    this.init()

  // ... 中间省略 ...

})

// 监听滚动保存位置
window.addEventListener("scroll", () => {
  appRouter.saveScrollPosition()
})

与 hash 路由的对比

项目hash 路由pushState 路由
URL 形式/#/profile/profile
是否触发完整刷新
是否需要服务器配置是(需要兜底到入口文件)
SEO 友好较差较好
支持老旧浏览器需 HTML5 支持
实现复杂度简单中等
状态管理通过 URL通过 state 对象

常见问题与解决方案

1. 页面刷新后 404

问题:使用 pushState 后,刷新页面会向服务器请求该 URL,可能导致 404。

解决方案:服务器端配置回退到入口页面。

nginx
# Nginx 配置
location / {
  try_files $uri $uri/ /index.html;
}
javascript
// Node.js/Express 配置
app.get("*", (req, res) => {
  res.sendFile(path.join(__dirname, "index.html"))
})

2. 状态丢失

问题:页面刷新后,状态对象可能丢失或不完整。

解决方案:将关键状态存储在 URL 或 localStorage 中。

javascript
// 将状态同步到 URL 参数
function pushStateWithSync(state, path) {
  const searchParams = new URLSearchParams(state)
  const url = `${path}?${searchParams}`
  history.pushState(state, "", url)
}

// 从 URL 恢复状态
function restoreStateFromURL() {
  const params = new URLSearchParams(location.search)
  const state = Object.fromEntries(params)
  return state
}

3. 内存泄漏

问题:状态对象存储过多数据导致内存问题。

解决方案:只存储必要的状态,使用引用 ID 替代完整数据。

javascript
// ❌ 不推荐:存储完整数据
history.pushState({ users: largeUserArray }, "", "/users")

// ✅ 推荐:只存储必要信息
history.pushState({ userPage: 1, userIds: users.map((u) => u.id) }, "", "/users")

4. 滚动位置异常

问题:页面导航后滚动位置不正确。

解决方案:手动管理滚动恢复。

javascript
// 方案1:使用 scrollRestoration
if ('scrollRestoration' in history) {
  history.scrollRestoration = 'manual'
  
  window.addEventListener('popstate', (e) => {
    if (e.state && e.state.scrollY !== undefined) {
      // 使用 setTimeout 确保页面渲染完成
      setTimeout(() => {
        window.scrollTo(0, e.state.scrollY)
      }, 0)
    }
  })
}

// 方案2:在导航前保存滚动位置
function navigateWithScroll(path, state = {}) {
  const currentState = {
    ...history.state,
    scrollY: window.scrollY
  }
  
  // 更新当前条目的滚动位置
  history.replaceState(currentState, '', location.href)
  
  // 导航到新页面
  history.pushState(state, '', path)
}

5. 移动端兼容性问题

问题:某些移动端浏览器的 popstate 事件行为不一致。

解决方案

javascript
let isPopstateSupported = true

// 检测 popstate 支持
try {
  window.addEventListener('popstate', function() {
    isPopstateSupported = true
  })
  
  // 触发测试
  history.pushState({}, '', location.href)
  history.back()
} catch (e) {
  isPopstateSupported = false
}

// 降级处理
if (!isPopstateSupported) {
  // 使用 hashchange 作为降级方案
  window.addEventListener('hashchange', handleRouteChange)
}

最佳实践

1. 提供真实的服务器路由

确保所有通过 pushState() 形成的 URL 都能被服务器正常处理,避免用户刷新页面后出现 404。

2. 限制状态对象大小

仅存储还原视图所需的最小数据,避免超过浏览器限制。

javascript
// 使用函数检查状态大小
function getStateSize(state) {
  return new Blob([JSON.stringify(state)]).size
}

const state = { data: "..." }
if (getStateSize(state) > 500000) {
  // 500KB 限制
  console.warn("状态对象过大,考虑减少数据")
}

3. 结合滚动管理

popstate 中恢复滚动位置,或启用 scrollRestoration

javascript
// 方式1:使用浏览器默认行为
history.scrollRestoration = "auto"

// 方式2:手动管理
history.scrollRestoration = "manual"
window.addEventListener("popstate", (event) => {
  if (event.state?.scrollY) {
    window.scrollTo({ top: event.state.scrollY, behavior: "smooth" })
  }
})

4. 记录导航来源

在状态对象中保存来源信息以实现更平滑的动画与过渡。

javascript
function navigate(path, options = {}) {
  const state = {
    from: location.pathname,
    to: path,
    direction: options.direction || "forward",
    timestamp: Date.now()
  }
  history.pushState(state, "", path)
}

5. 回退兜底

导航失败时提供提示或退回首页,保证用户体验。

javascript
function safeBack() {
  if (history.length > 1) {
    history.back()
  } else {
    // 没有历史记录时的回退方案
    location.href = "/"
  }
}

6. 提供导航反馈

在导航过程中提供加载指示器,改善用户体验。

javascript
async function navigateWithFeedback(path) {
  showLoadingIndicator()
  
  try {
    history.pushState({}, '', path)
    await renderPage(path)
  } catch (error) {
    console.error('导航失败:', error)
    showError('页面加载失败')
  } finally {
    hideLoadingIndicator()
  }
}

总结

history 对象是单页应用路由的核心,掌握其 API 对于现代前端开发至关重要:

API用途是否创建历史记录
history.back()后退一页-
history.forward()前进一页-
history.go(n)跳转 n 步-
history.pushState()添加历史条目
history.replaceState()替换当前条目
history.state获取当前状态-
history.scrollRestoration滚动恢复设置-
history.length历史栈长度-

配合 popstate 事件和状态对象,可以实现可控的前进/后退体验,为用户提供流畅的单页应用导航。在实际开发中,需要注意浏览器兼容性、安全性限制和性能优化,确保应用的稳定性和用户体验。