核心是用 document.querySelectorAll 提取 h1–h6 标题,自动生成唯一 id,用 innerText 避免内嵌标签干扰,结合 IntersectionObserver(配置 rootMargin 与缓存激活项)实现精准高亮,scrollIntoView 跳转需补偿头部高度并降级兼容 Safari 和 IE。

用 document.querySelectorAll 提取标题并生成 TOC 结构
核心是识别页面中所有层级标题(h1–h6),按 DOM 顺序提取文本和 id,再动态插入到指定容器中。必须确保每个标题有唯一 id,否则锚点跳转会失效;若没有,得在遍历时自动生成(比如基于文本内容哈希或序号拼接)。
常见错误:直接用 textContent 取值却忽略内嵌标签(如 <strong> 或图标 <svg>),导致 TOC 文字异常。应改用 innerText 或先克隆节点再清空子元素。
- 只处理当前视口内或已渲染完成的标题,避免对动态加载内容(如 SPA 路由切换后)漏采
- 层级缩进靠 CSS 的
margin-left或padding-left控制,不建议用嵌套<ul>增加结构复杂度 - 生成链接时 href 必须带
#前缀,且值与目标标题id完全一致(区分大小写、空格、特殊字符)
滚动时用 IntersectionObserver 触发高亮更新
比监听 scroll 事件更轻量、更可靠。IntersectionObserver 能精准判断哪个标题进入/离开视口,尤其适合长文档。关键在于配置 rootMargin:设为 "0px 0px -50% 0px" 表示“当标题中点进入视口时即视为可见”,避免滚动过快时高亮滞后或错位。
容易踩的坑:threshold: [0] 不够用——它只在元素刚接触视口边缘时触发,而实际需要的是“稳定可见”状态。应配合 isIntersecting 和一个缓存变量,记录当前“最顶部且完全可见”的标题。
立即学习“前端免费学习笔记(深入)”;
- 多个同级标题同时可见时,取 offsetTop 最小的那个作为激活项
- 观察器需监听的是标题元素本身,不是其父容器
- 首次加载时手动触发一次高亮同步,否则初始位置可能无高亮
scrollIntoView 点击跳转时的平滑与偏移控制
点击 TOC 条目跳转,默认会把目标标题顶到视口最顶端,常被导航栏遮挡。必须用 { behavior: 'smooth', block: 'start' } 并配合 window.scrollBy(0, -80) 补偿固定头部高度。
但要注意:如果页面启用了 scroll-behavior: smooth 全局 CSS,再调用 scrollIntoView 可能触发两次滚动动画。建议关闭 CSS 行为,统一由 JS 控制。
- 跳转前检查目标元素是否存在,避免
Cannot read property 'scrollIntoView' of null - 移动端 Safari 对
scrollIntoView支持不稳定,可降级为window.scrollTo({ top: el.offsetTop - 80 }) - 跳转后需重置 IntersectionObserver 的状态,否则可能因滚动未完成导致高亮错乱
兼容性与性能边界必须手动兜底
IE 完全不支持 IntersectionObserver 和 scrollIntoView,必须提供降级方案:用 getBoundingClientRect() + scroll 事件轮询模拟可见性判断,虽然性能差,但至少功能可用。现代项目中若已放弃 IE,也要注意 Safari 15.4 之前版本对 rootMargin 百分比单位的支持不完整。
TOC 条目过多(比如 >200 个)时,频繁 DOM 插入会卡顿。应使用 DocumentFragment 批量插入,或虚拟滚动截取可视区域附近的 10–15 个标题。
- 生成 TOC 前先做节流,防止重复执行(如多次调用初始化函数)
- 所有事件监听器(scroll、resize)都要记得
removeEventListener清理,尤其单页应用中组件卸载时 - 高亮样式不要依赖
:target伪类——它无法响应 JS 滚动,也不支持“最近可见”逻辑
实际最难的部分不是生成目录,而是让高亮行为在各种滚动节奏、缩放比例、iframe 嵌套、字体加载延迟下都保持稳定。这些细节往往要靠反复在真机上拖拽验证,而不是看代码是否“理论上成立”。



















