DAY_11JavaScript BOM与DOM深度解析:底层原理与工程实践(上)

JavaScript BOM与DOM深度解析:底层原理与工程实践

摘要:浏览器对象模型(BOM)与文档对象模型(DOM)构成了JavaScript与浏览器交互的核心抽象层。本文从ECMAScript规范出发,深入剖析BOM/DOM的底层实现机制、内存模型、事件循环架构,以及在实际工程中的最佳实践。通过对API设计模式的解构和性能优化策略的探讨,帮助开发者建立从原理到应用的完整认知体系。
关键词:JavaScript运行时、BOM/DOM架构、事件驱动模型、性能优化、前端工程化

目录

  1. 内置对象回顾与进阶

  2. DOM核心概念

    • [2.1 DOM规范架构演进](#2.1 DOM规范架构演进)
    • [2.2 DOM节点类型层次结构](#2.2 DOM节点类型层次结构)
    • [2.3 DOM渲染流水线与内存模型](#2.3 DOM渲染流水线与内存模型)
    • [2.4 DOM操作的黄金四件套](#2.4 DOM操作的黄金四件套)
    • [2.5 DOM事件模型:捕获、冒泡与委托](#2.5 DOM事件模型:捕获、冒泡与委托)
    • [2.6 现代 Observer API](#2.6 现代 Observer API)
    • [2.7 DOM 查询策略与集合类型](#2.7 DOM 查询策略与集合类型)
    • [2.8 原生 API 与现代框架映射](#2.8 原生 API 与现代框架映射)
    • [2.9 Shadow DOM 与 Web Components](#2.9 Shadow DOM 与 Web Components)
    • [2.10 classList 与 dataset 实战](#2.10 classList 与 dataset 实战)
    • [2.11 DOM 节点遍历与 DOMContentLoaded](#2.11 DOM 节点遍历与 DOMContentLoaded)
  3. BOM完整体系

    • BOM对象层次结构图
    • BOM与DOM关系图
    • [3.4 Window对象](#3.4 Window对象)
      • [① 弹框系列](#① 弹框系列)
      • [② 窗口打开与关闭](#② 窗口打开与关闭)
      • [③ 页面滚动控制](#③ 页面滚动控制)
      • [④ 定时器机制](#④ 定时器机制)
      • [⑤ Window对象属性总结](#⑤ Window对象属性总结)
    • [3.5 History对象](#3.5 History对象)
      • [3.5.1 History API 与 SPA 路由](#3.5.1 History API 与 SPA 路由)
    • [3.6 Location对象](#3.6 Location对象)
    • [3.7 Navigator对象](#3.7 Navigator对象)
    • [3.8 Screen对象](#3.8 Screen对象)
    • [3.9 Web Storage:localStorage 与 sessionStorage](#3.9 Web Storage:localStorage 与 sessionStorage)
    • [3.10 同源策略与跨窗口通信](#3.10 同源策略与跨窗口通信)
  4. 实战案例详解

  5. 性能优化与最佳实践

    • [5.1 定时器使用注意事项](#5.1 定时器使用注意事项)
    • [5.2 DOM操作优化](#5.2 DOM操作优化)
      • [5.2.2 防抖与节流](#5.2.2 防抖与节流)
      • [5.2.3 强制同步布局(Layout Thrashing)](#5.2.3 强制同步布局(Layout Thrashing))
    • [5.3 BOM API兼容性处理](#5.3 BOM API兼容性处理)
    • [5.4 安全注意事项](#5.4 安全注意事项)
    • [5.5 Performance API 与 Web Vitals](#5.5 Performance API 与 Web Vitals)
    • [5.6 requestAnimationFrame 动画帧调度](#5.6 requestAnimationFrame 动画帧调度)
  6. 事件循环全景:宏任务、微任务与渲染帧

  7. [BOM/DOM 理论体系深化](#BOM/DOM 理论体系深化)

    • [9.1 三层抽象:ECMAScript、Web IDL、宿主对象](#9.1 三层抽象:ECMAScript、Web IDL、宿主对象)
    • [9.2 HTML 解析与 DOM 构建理论](#9.2 HTML 解析与 DOM 构建理论)
    • [9.3 渲染引擎:样式、布局、层叠与合成](#9.3 渲染引擎:样式、布局、层叠与合成)
    • [9.4 事件系统:分发、默认行为与命中测试](#9.4 事件系统:分发、默认行为与命中测试)
    • [9.5 浏览上下文、Realm 与 WindowProxy](#9.5 浏览上下文、Realm 与 WindowProxy)
    • [9.6 会话历史、导航与 bfcache](#9.6 会话历史、导航与 bfcache)
    • [9.7 安全信任模型:SOP、CSP 与 Trusted Types](#9.7 安全信任模型:SOP、CSP 与 Trusted Types)
    • [9.8 可达性与内存:DOM 为何容易泄漏](#9.8 可达性与内存:DOM 为何容易泄漏)
  8. 经典业务场景与可实现的业务

    • [6.1 完整业务场景架构图](#6.1 完整业务场景架构图)
    • [6.2 可实现的核心业务清单](#6.2 可实现的核心业务清单)
    • [6.3 综合业务案例实现流程](#6.3 综合业务案例实现流程)
    • [6.4 性能监控业务](#6.4 性能监控业务)
    • [6.5 设备适配业务](#6.5 设备适配业务)
    • [6.6 Web Components 与 Shadow DOM](#6.6 Web Components 与 Shadow DOM)
    • [6.7 主流网站经典场景与业务价值](#6.7 主流网站经典场景与业务价值)
      • [6.7.4 按业务目标再归类](#6.7.4 按业务目标再归类)
  9. 总结

附录. 工程化实践补充

  • [A.1 代码风格:ES5 与 ES6+ 对照](#A.1 代码风格:ES5 与 ES6+ 对照)
  • [A.2 TypeScript 核心类型定义](#A.2 TypeScript 核心类型定义)
  • [A.3 单元测试示例](#A.3 单元测试示例)
  • [A.4 性能基准参考数据](#A.4 性能基准参考数据)
  • [A.5 错误处理模式](#A.5 错误处理模式)

关于文中代码风格 :正文部分示例沿用课程场景的 ES5 写法 (var、function 声明),便于与课堂笔记对照。生产工程请采用 [附录 A.1](#附录 A.1) 的 ES6+ 写法;新增工具函数与测试均以现代语法为准。


1. JavaScript内置对象机制深度剖析

1.1 Function对象:函数式编程的核心抽象

Function对象不仅是JavaScript中所有函数的构造函数,更是函数式一等公民(First-Class Citizen)特性的具体体现。从ECMAScript规范角度,Function对象具备以下核心特征:

  1. 原型链特性:Function.prototype是所有函数的原型对象,包含call、apply、bind等核心方法
  2. 闭包机制:通过\[Environment]内部槽实现词法作用域的持久化
  3. 动态this绑定:通过\[Call]和\[Construct]内部方法实现不同的执行语义
1.1.1 函数类型体系架构

运行时特征
ECMAScript规范视角
不支持
继承外层
透传
不支持
预设
Function Objects
Ordinary Functions
Arrow Functions
Bound Functions
Built-in Functions
可调用性 Callable
可构造性 Constructible
this绑定机制
原型链继承

设计模式分析:

设计模式 应用场景 实现机制
原型模式 方法复用 Function原型链继承
代理模式 this控制 call/apply/bind实现
装饰器模式 函数增强 高阶函数组合
观察者模式 异步通知 事件回调机制
1.1.2 this绑定的底层机制

this的指向并非简单的"调用对象决定",而是由JavaScript引擎的执行上下文(Execution Context)中的ThisBinding组件决定。根据ECMAScript规范,this绑定分为以下五种类型:
渲染错误: Mermaid 渲染失败: Parse error on line 14: ...F --> J\[Construct]内部方法] G --> -----------------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'SHAPE_DATA', 'STYLE_SEPARATOR', 'START_LINK', 'LINK', 'LINK_ID', got 'UNICODE_TEXT'

绑定优先级(从高到低):

  1. new绑定:构造函数调用优先级最高
  2. 显式绑定:call/apply/bind硬编码
  3. 隐式绑定:对象方法调用
  4. 默认绑定:独立函数调用
1.1.3 call/apply/bind的规范级实现

执行上下文 Function对象 调用者 执行上下文 Function对象 调用者 call()执行流程 bind()执行流程 func.call(thisArg, ...args) 准备thisArg (null/undefined → global) 创建新的执行上下文 设置ThisBinding = thisArg 展开...args为参数列表 执行\[Call]内部方法 返回执行结果 func.bind(thisArg, ...args) 创建BoundFunction对象 存储\[BoundTargetFunction] 存储\[BoundThis] 存储\[BoundArguments] 返回BoundFunction

核心属性对比:

特性 call() apply() bind()
执行时机 立即执行 立即执行 延迟执行
参数传递 逐个传递 数组展开 预设参数
返回值 函数返回值 函数返回值 新函数
this永久性 临时 临时 永久绑定
性能开销 最小 小(需展开数组) 中(创建闭包)
内部槽 - - \[BoundTargetFunction] \[BoundThis] \[BoundArguments]

独立函数调用
对象方法调用
call/apply/bind
new 构造函数
箭头函数
函数调用
调用方式
this → window/undefined

严格模式
this → 调用对象
this → 指定对象
this → 新实例
this → 外层作用域this
默认绑定
隐式绑定
显式绑定
new绑定
箭头函数绑定

call/apply/bind 对比图

bind
函数.bind

this, arg1, arg2
返回新函数
apply
函数.apply

this, args
call
函数.call

this, arg1, arg2
立即执行
返回函数

需手动调用
参数逐个传递
参数数组传递
参数预设/this绑定

完整示例代码
javascript 复制代码
function func(num01, num02) {
    console.log('func被调用了:', num01 * num02, this);
    return num01 + num02;
}

console.log(func.length);
console.log(parseInt.length);
console.log(Array.length);
console.log(isNaN.length);
console.log([].join.length);

代码解释: length属性返回函数定义时声明的形参个数。func有2个形参,parseInt有1个形参,Array构造函数有1个形参,join方法有1个形参(分隔符)。


javascript 复制代码
var user = {
    name: '曹操',
    getInfo: func
};

func(10, 20);

user.getInfo(4, 6);

代码解释: 函数中this的指向取决于函数的调用方式。作为独立函数调用时,this默认指向window;作为对象方法调用时,this指向调用该方法的对象(user)。


javascript 复制代码
var res = func.call({name: '高小乐'}, 10, 6);
console.log(res);

user.getInfo.call([10, 20, 30], 8, 5);

代码解释: call()方法可以借用其他对象的方法,并灵活改变this指向。第一个参数设置this指向,后续参数逐个传递给函数。虽然通过user.getInfo调用,但this被强制指向数组[10, 20, 30]。


javascript 复制代码
var res1 = func.apply('hello', [19, 2]);
console.log(res1);

user.getInfo.apply(new Date());

代码解释: apply()与call()的区别在于第二个参数必须是数组,数组元素会被展开作为函数参数。this指向字符串'hello'(被包装成String对象),数组[19, 2]中的元素分别传递给num01和num02。apply常用于参数数组展开的场景。


javascript 复制代码
var str = 'Hello高小乐';

[].forEach.call(str, function(item, index, arr) {
    console.log(item, index);
});

代码解释: 这是call()的高级用法,让字符串"借用"数组的遍历方法。字符串被当作类数组对象,forEach可以遍历每个字符(H 0, e 1, l 2, l 3, o 4, 高 5, 小 6, 乐 7)。


javascript 复制代码
var str1 = [].reduceRight.call(str, function(prev, item) {
    return prev + item;
});
console.log(str1);

代码解释: reduceRight()从右向左遍历,配合call()实现字符串反转。每个字符被拼接到prev前面,最终输出"乐高小olleH"。


javascript 复制代码
[].forEach.apply(str, [function(item, index, arr) {
    console.log(item, index);
}]);

代码解释: 这种写法展示了apply()的特殊用法,虽然不太常用,但体现了JavaScript函数式编程的特性。apply将回调函数作为forEach的this(即str),输出结果与示例5相同。


javascript 复制代码
var fn01 = func.bind([10, 20, 30, 40, 50]);
console.log(fn01(10, 20));

代码解释: bind()创建一个新函数,this被永久绑定到[10, 20, 30, 40, 50]。无论fn01如何调用,this始终指向该数组,常用于事件处理函数中保持上下文。


javascript 复制代码
var fn02 = func.bind([10, 20, 30], 10000);
console.log(fn02(2));

var fn03 = func.bind([10, 20, 30], 100, 200);
console.log(fn03());

代码解释: bind()的参数预设特性可以实现偏函数(Partial Function)。第一个参数10000被预设给num01,调用时只需传第二个参数;两个参数都被预设后,调用时不需要传参数。


实战案例1:字符串转换工具(短横线转小驼峰)
javascript 复制代码
function toCamel(str) {
    return str.split('-').map(function(item, index) {
        return index === 0
            ? item.toLowerCase()
            : item[0].toUpperCase() + item.slice(1).toLowerCase();
    }).join('');
}

console.log(toCamel('get-element-by-id'));
console.log(toCamel('set-name-by-class-name'));

代码解释: 将短横线分隔的字符串转为小驼峰形式。执行过程:

  1. split('-')将字符串按短横线分割成数组
  2. map()遍历数组,第一个单词全小写,后续单词首字母大写其余小写
  3. join('')将数组重新拼接成字符串

'get-element-by-id' → ['get', 'element', 'by', 'id'] → ['get', 'Element', 'By', 'Id'] → 'getElementById'


1.2 Global 对象与浏览器环境

ECMAScript标准规定的全局对象,在浏览器环境中,window对象即为Global的实现。像Array、Number、String、isNaN、isFinite等都是Global的属性。

完整示例代码
javascript 复制代码
var msg = 'var address = "上海"';
eval(msg);
console.log(address);

代码解释: eval()可以将字符串解析为可执行代码,声明了全局变量address。但由于安全风险和性能问题,应避免在生产环境使用。


javascript 复制代码
var url = 'http://www.atguigu.com/image/小乐.html?name=101&age=20';
console.log(url);

var url01 = encodeURI(url);
console.log(url01);

代码解释: encodeURI()用于编码完整的URL,只编码非URL字符(如中文),保留URL的结构符号。中文"小乐"被编码为%E5%B0%8F%E4%B9%90,而http://、?、&、=等URL符号保持不变。


javascript 复制代码
var url02 = decodeURI(url01);
console.log(url02);

代码解释: decodeURI()是encodeURI()的逆操作,将编码后的URL还原为原始形式。


2. DOM核心概念与规范体系

2.1 DOM规范架构演进

DOM(Document Object Model)是由W3C标准化的跨平台语言中立接口,其规范体系经历了四个主要发展阶段:
浏览器实现
DOM规范演进
废弃
DOM Level 1

1998年

核心模型
DOM Level 2

2000年

事件/样式
DOM Level 3

2004年

加载/验证
DOM4 / WHATWG

Living Standard

持续演进
Blink

Chrome/Edge
Gecko

Firefox
WebKit

Safari
Trident

IE已废弃

2.2 DOM节点类型层次结构

<<abstract>>
Node
+nodeType: number
+nodeName: string
+nodeValue: string
+childNodes: NodeList
+parentNode: Node
+ownerDocument: Document
+appendChild(child)
+removeChild(child)
+insertBefore(new, ref)
+cloneNode(deep)
+contains(node)
Document
+documentElement: Element
+body: HTMLElement
+head: HTMLElement
+doctype: DocumentType
+getElementById(id)
+getElementsByTagName(tag)
+getElementsByClassName(cls)
+querySelector(sel)
+createElement(tag)
+createTextNode(txt)
Element
+tagName: string
+className: string
+id: string
+innerHTML: string
+attributes: NamedNodeMap
+getAttribute(name)
+setAttribute(name, value)
+removeAttribute(name)
+querySelector(sel)
+querySelectorAll(sel)
Text
+data: string
+length: number
+wholeText: string
+splitText(offset)
Comment
DocumentFragment
DocumentType
HTMLElement
SVGElement
HTMLDivElement
HTMLSpanElement
HTMLAnchorElement

节点类型常量规范:

节点类型 常量名 nodeType nodeName nodeValue
Element ELEMENT_NODE 1 元素标签名 null
Attr ATTRIBUTE_NODE 2 属性名 属性值
Text TEXT_NODE 3 #text 文本内容
CDATASection CDATA_SECTION_NODE 4 #cdata-section CDATA内容
EntityReference ENTITY_REFERENCE_NODE 5 实体引用名 -
Entity ENTITY_NODE 6 实体名 -
ProcessingInstruction PROCESSING_INSTRUCTION_NODE 7 target data
Comment COMMENT_NODE 8 #comment 注释内容
Document DOCUMENT_NODE 9 #document null
DocumentType DOCUMENT_TYPE_NODE 10 doctype名 null
DocumentFragment DOCUMENT_FRAGMENT_NODE 11 #document-fragment null
Notation NOTATION_NODE 12 符号名 -

2.3 DOM渲染流水线与内存模型

绘制合成
布局计算
渲染构建
样式处理
解析阶段
HTML字节流
词法分析 Tokenizer
语法分析 Parser
DOM树构建
CSS解析
样式规则计算
Attach渲染对象
渲染树 RenderTree
布局 Layout
层分层 Layer
绘制 Paint
合成 Composite
位图输出

内存布局分析:
堆内存
弱引用
强引用
强引用
占用
占用
引用关系
DOM树节点
渲染对象
布局对象
层对象 Layer
~10-50MB/千节点
~20-100MB/千节点

2.4 DOM操作的黄金四件套

javascript 复制代码
var boxEle = document.getElementById('box');

boxEle.onclick = function() {
    boxEle.style.backgroundColor = '#900';
    boxEle.style.color = '#fff';
    boxEle.style.fontSize = '14px';

    boxEle.innerHTML = '哈哈哈,我被点了一下!';
};

代码解释: DOM操作的黄金四步:获取元素→绑定事件→修改样式→更新内容。点击后背景变红、文字变白、字体变小、内容改变,是所有交互效果的基础。


完整示例:交互式盒子

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>DOM交互示例</title>
    <style>
        #box {
            width: 600px;
            padding: 20px;
            height: 250px;
            background: #ccc;
        }
    </style>
</head>
<body>
    <h1>JavaScript</h1>
    <div id="box">流会法,个会惶言盲亲我,觉就许...</div>

    <script>
        var boxEle = document.getElementById('box');

        boxEle.onclick = function() {
            boxEle.style.backgroundColor = '#900';
            boxEle.style.color = '#fff';
            boxEle.style.fontSize = '14px';

            boxEle.innerHTML = '哈哈哈,我被点了一下!';
        };
    </script>
</body>
</html>

代码解释: 完整的DOM交互示例。点击灰色盒子后,背景变红(#900)、文字变白(#fff)、字体变小(14px)、内容改变,展示了DOM操作的实际效果。


2.5 DOM事件模型:捕获、冒泡与委托

DOM Events 规范(DOM Level 2+)定义了**事件流(Event Flow)**的三阶段模型,这是理解 addEventListener、事件委托与性能优化的基础。
监听器 目标元素 Body Document Window 监听器 目标元素 Body Document Window ① 捕获阶段 Capture(由外向内) ② 目标阶段 Target ③ 冒泡阶段 Bubble(由内向外) 事件向下传播 事件向下传播 事件到达目标 触发目标上的监听器 事件向上传播 事件向上传播 传播结束

三阶段对比:

阶段 传播方向 event.eventPhase 典型用途
捕获 window → target 1 (CAPTURING_PHASE) 全局拦截、下拉菜单关闭
目标 停在 target 2 (AT_TARGET) 元素自身逻辑
冒泡 target → window 3 (BUBBLING_PHASE) 事件委托(最常用)
2.5.1 addEventListener 与属性绑定的工程差异
特性 element.onclick = fn addEventListener(type, fn, options)
绑定数量 仅一个处理器(后者覆盖前者) 可注册多个独立监听器
捕获阶段 不支持 options.capture === true
被动监听 不支持 { passive: true } 告知浏览器不调用 preventDefault
一次性 不支持 { once: true } 执行后自动移除
移除方式 赋值为 null removeEventListener 需传入同一函数引用

passive 与滚动性能 :在 touchstart/wheel 等高频事件上,浏览器默认可能将监听器视为 passive(Chrome 56+)。若需阻止默认滚动,必须显式 { passive: false },否则控制台会警告且 preventDefault() 无效。

javascript 复制代码
// 推荐:可组合、可移除、支持委托
document.getElementById('list').addEventListener('click', function(e) {
    var item = e.target.closest('[data-id]');
    if (!item || !this.contains(item)) return;
    console.log('选中项:', item.dataset.id);
}, false); // false = 冒泡阶段(默认)
2.5.2 事件委托:以空间换时间的经典模式

原理:利用冒泡阶段,在祖先节点统一处理子节点事件,避免为 N 个子元素各绑 N 个监听器。

javascript 复制代码
var tbody = document.querySelector('#user-table tbody');

tbody.addEventListener('click', function(e) {
    var btn = e.target.closest('button[data-action]');
    if (!btn) return;

    var action = btn.dataset.action;
    var row = btn.closest('tr');
    var id = row && row.dataset.id;

    if (action === 'edit') editUser(id);
    if (action === 'delete') deleteUser(id);
});

适用场景:动态列表、表格操作列、无限滚动列表------子节点增删时无需重新绑定。

注意事项:

  • e.target 可能是文本节点或嵌套子元素,使用 closest() 向上匹配
  • 不是所有事件都冒泡(如 focus/blur 不冒泡,可用 focusin/focusout)
  • 委托层不宜过深,否则每次点击都要遍历祖先链
2.5.3 DOM 事件与事件循环的协作

用户交互事件(click、input 等)由浏览器渲染进程 捕获后,将回调封装为宏任务(Task)入队;Promise.then、MutationObserver 等为微任务(Microtask),在当前宏任务清空后、下一个宏任务前全部执行。
用户点击
浏览器合成事件对象
宏任务:dispatch 事件
执行监听器同步代码
微任务队列清空
可能触发重排/重绘
requestAnimationFrame 回调

工程启示 :在事件处理器中避免长时间同步计算;批量 DOM 写入可配合 requestAnimationFrame 或 DocumentFragment,防止阻塞后续事件与帧渲染。


2.6 现代 Observer API

除传统事件外,WHATWG 与 W3C 提供了异步观察器,将「何时处理」交给浏览器调度,减少 scroll/resize 轮询。

API 观察对象 典型场景 与 BOM/DOM 关系
IntersectionObserver 元素与视口交叉 图片懒加载、曝光埋点 替代 scroll + getBoundingClientRect
MutationObserver DOM 树变更 第三方组件监控、富文本同步 微任务回调,批量记录 mutations
ResizeObserver 元素尺寸变化 图表自适应、容器查询 比 window.resize 更细粒度
javascript 复制代码
// 图片懒加载:进入视口前不请求真实 src
var io = new IntersectionObserver(function(entries) {
    entries.forEach(function(entry) {
        if (!entry.isIntersecting) return;
        var img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute('data-src');
        io.unobserve(img);
    });
}, { rootMargin: '200px' }); // 提前 200px 预加载

document.querySelectorAll('img[data-src]').forEach(function(img) {
    io.observe(img);
});

2.7 DOM 查询策略与集合类型

元素查询是 DOM 编程的入口。不同 API 在时间复杂度、返回值类型、是否 Live 上差异显著,直接影响性能与正确性。

API 返回类型 Live? 匹配方式 性能特征
getElementById(id) `Element null` - ID 哈希表
getElementsByClassName HTMLCollection ✓ Live 类名 文档变更时自动更新
getElementsByTagName HTMLCollection ✓ Live 标签名 同上
querySelector(sel) `Element null` - CSS 选择器
querySelectorAll(sel) 静态 NodeList ✗ CSS 选择器 快照,后续 DOM 变更不影响

Live vs Static 的工程陷阱:

javascript 复制代码
var items = document.getElementsByClassName('item'); // Live HTMLCollection
for (var i = 0; i < items.length; i++) {
    items[i].classList.remove('item'); // 每移除一个,length 减 1,可能跳过元素!
}

// 正确:倒序遍历,或转为静态数组
Array.prototype.slice.call(items).forEach(function(el) {
    el.classList.remove('item');
});

querySelector 与 getElementById 选型:

  • 已知唯一 id → 优先 getElementById(不触发选择器引擎全树扫描)
  • 复杂组合条件(main .card[data-active])→ querySelector(All)
  • 在同一父节点 内反复查询 → 缓存父节点:var root = document.getElementById('app'); root.querySelector('.btn')

辅助 API:

方法 作用
element.closest(selector) 向上匹配祖先,事件委托核心
element.matches(selector) 当前节点是否匹配,过滤用
element.contains(node) 是否包含子节点(含自身 false)

2.8 原生 API 与现代框架映射

理解框架并非替代 BOM/DOM,而是在其之上建立声明式抽象层。读源码或调试生产问题时,必须能还原到原生 API。
React 18+
Vue 3
原生层
document.querySelector
addEventListener
history.pushState
classList / style
ref / template ref
v-on / @click
vue-router history模式
响应式 class 绑定
useRef + ref.current
合成事件 SyntheticEvent
react-router useNavigate
className / style prop

能力 原生 Vue 3 React
元素引用 document.getElementById ref="el" / useTemplateRef useRef(null) → .current
事件 addEventListener @click(编译为原生监听) 根节点委托 + 合成事件池
路由 hash / pushState vue-router:createWebHistory() react-router:createBrowserRouter
列表渲染 手动 createElement v-for → patch DOM map → Virtual DOM diff
状态驱动 UI 手动改 innerHTML 响应式 → 自动 patch setState → reconcile

合成事件(React)与原生事件的区别 :React 17+ 将委托挂载到根容器 而非 document,事件对象在冒泡到根时统一包装为 SyntheticEvent,e.nativeEvent 可访问原生事件。调试时注意 stopPropagation 在合成层与原生层的行为一致性问题。

Vue 的 nextTick :DOM 更新异步批处理,等价于在微任务中等待 Virtual DOM patch 完成后再读布局------底层常结合 Promise 微任务与 MutationObserver。


2.9 Shadow DOM 与 Web Components

Shadow DOM 在元素内部维护独立的 DOM 子树,样式与 id 与主文档隔离,是 Web Components 封装的基础。
Shadow Root 封闭树
Light DOM 主文档
投影
attachShadow mode: open
custom-card
slot 插槽
内部结构 / 样式
主文档中的子节点

javascript 复制代码
class UserCard extends HTMLElement {
    constructor() {
        super();
        var shadow = this.attachShadow({ mode: 'open' });
        shadow.innerHTML =
            '<style>:host { display: block; border: 1px solid #ccc; }</style>' +
            '<h3><slot name="title">默认标题</slot></h3>' +
            '<p><slot></slot></p>';
    }
}
customElements.define('user-card', UserCard);
概念 说明
:host 选中 Shadow 宿主元素本身
::slotted() 样式化被投影进来的 slotted 节点
mode: 'closed' 外部无法通过 element.shadowRoot 访问
事件重定向 部分事件在 Shadow 边界上 event.target 会「重定向」到宿主

与框架关系 :Vue 3 的 scoped、React 的 CSS Modules 解决的是样式隔离 ;Shadow DOM 解决的是 DOM 树隔离 + 原生组件复用。Element Plus、Lit、Stencil 等库在底层均可能使用 Shadow DOM。


2.10 classList 与 dataset 实战

2.10.1 classList:类名的最优操作方式

element.classList 返回 DOMTokenList 对象,比直接操作 className 字符串更安全、更语义化。

方法 说明 示例
add(...classes) 添加一个或多个类 el.classList.add('active', 'visible')
remove(...classes) 移除一个或多个类 el.classList.remove('hidden')
toggle(class, force?) 有则移除,无则添加 el.classList.toggle('open')
contains(class) 是否含有该类,返回 boolean el.classList.contains('active')
replace(old, new) 替换类名 el.classList.replace('btn-blue', 'btn-red')
item(index) 按索引取类名 el.classList.item(0)
length 类的数量 el.classList.length
javascript 复制代码
var btn = document.getElementById('btn');

// 切换激活状态(最常用)
btn.onclick = function() {
    btn.classList.toggle('active');
};

// 根据条件动态添加/移除
function setLoading(el, isLoading) {
    if (isLoading) {
        el.classList.add('loading');
        el.classList.remove('idle');
    } else {
        el.classList.remove('loading');
        el.classList.add('idle');
    }
}

// toggle 的 force 参数:true = 强制添加,false = 强制移除
var isNight = true;
document.body.classList.toggle('dark-mode', isNight);

// 判断后再操作(避免重复添加的无意义调用)
if (!btn.classList.contains('disabled')) {
    btn.classList.add('disabled');
    btn.setAttribute('disabled', 'disabled');
}

// 遍历所有类名
Array.prototype.forEach.call(btn.classList, function(cls) {
    console.log(cls);
});

与 className 的对比:

javascript 复制代码
// ❌ 旧写法:容易破坏已有类名
element.className = element.className + ' active'; // 可能出现重复
element.className = element.className.replace('active', ''); // 不稳健

// ✅ 新写法:classList 保证幂等性
element.classList.add('active');
element.classList.remove('active');
2.10.2 dataset:自定义数据属性

element.dataset 提供对 data-* HTML 属性的读写访问,是"将数据嵌入 DOM"的官方方式,常与事件委托配合使用。

命名规则 :HTML data-foo-bar → JS element.dataset.fooBar(短横线转小驼峰)

html 复制代码
<ul id="product-list">
    <li data-id="101" data-price="299" data-category="phone">手机 A</li>
    <li data-id="102" data-price="99"  data-category="earphone">耳机 B</li>
    <li data-id="103" data-price="1999" data-category="laptop">笔记本 C</li>
</ul>
javascript 复制代码
var list = document.getElementById('product-list');

// 读取 dataset
list.addEventListener('click', function(e) {
    var item = e.target.closest('li[data-id]');
    if (!item) return;

    var id       = item.dataset.id;       // '101'(始终是字符串)
    var price    = Number(item.dataset.price);  // 转数字
    var category = item.dataset.category; // 'phone'

    console.log('商品ID:', id, '价格:', price, '分类:', category);
});

// 写入 dataset(会同步到 HTML 属性)
var li = document.querySelector('li');
li.dataset.selected = 'true';    // → data-selected="true"
li.dataset.viewCount = '5';      // → data-view-count="5"

// 删除 dataset 属性
delete li.dataset.selected;

// 遍历所有 data-* 属性
Object.keys(li.dataset).forEach(function(key) {
    console.log(key, ':', li.dataset[key]);
});

与 getAttribute 的对比:

方式 写法 特点
element.getAttribute('data-id') 原始字符串操作 兼容所有属性,名称不转换
element.dataset.id 语义化读写 仅限 data-*,自动驼峰转换
javascript 复制代码
// 等价写法
var id1 = el.getAttribute('data-product-id');
var id2 = el.dataset.productId; // 推荐

2.11 DOM 节点遍历与 DOMContentLoaded

2.11.1 节点遍历 API

DOM 树导航分为节点级 (含文本/注释节点)和元素级(只含元素节点)两套属性。生产环境几乎总是用元素级:
节点级(含文本节点)
parentNode
childNodes
firstChild
lastChild
previousSibling
nextSibling
元素级(推荐)
parentElement
children
firstElementChild
lastElementChild
previousElementSibling
nextElementSibling

属性 说明 返回类型
element.children 所有直接子元素 HTMLCollection(Live)
element.childNodes 所有直接子节点(含文本) NodeList(Live)
element.firstElementChild 第一个子元素 `Element
element.lastElementChild 最后一个子元素 `Element
element.previousElementSibling 前一个兄弟元素 `Element
element.nextElementSibling 后一个兄弟元素 `Element
element.parentElement 父元素 `Element
element.parentNode 父节点(可能是 Document) `Node
javascript 复制代码
var ul = document.querySelector('ul');

// 遍历所有直接子元素
Array.prototype.forEach.call(ul.children, function(li, index) {
    console.log(index, li.textContent);
});

// 找到某元素的下一个兄弟元素
var current = document.getElementById('item2');
var next = current.nextElementSibling;
var prev = current.previousElementSibling;

// 向上查找特定祖先(等价于 closest,但手动版)
function findAncestor(el, tagName) {
    while (el && el.tagName !== tagName.toUpperCase()) {
        el = el.parentElement;
    }
    return el;
}

// children 是 Live 集合,删除时倒序遍历
var items = ul.children;
for (var i = items.length - 1; i >= 0; i--) {
    if (items[i].classList.contains('deleted')) {
        ul.removeChild(items[i]);
    }
}
2.11.2 document.readyState 与 DOMContentLoaded

document.readyState 反映页面加载的三个阶段,决定了脚本访问 DOM 的时机。

状态值 含义 触发事件
"loading" HTML 正在解析,DOM 未完整 ---
"interactive" HTML 解析完成,子资源仍在加载 DOMContentLoaded
"complete" 页面所有资源加载完毕 load
javascript 复制代码
// 方法一:监听 DOMContentLoaded(推荐,无需等待图片/样式)
document.addEventListener('DOMContentLoaded', function() {
    // DOM 已可用,可以安全操作元素
    var app = document.getElementById('app');
    console.log('DOM 就绪,子元素数:', app.children.length);
});

// 方法二:监听 readystatechange,兼容所有阶段
document.addEventListener('readystatechange', function() {
    console.log('当前状态:', document.readyState);
    // 'loading' → 'interactive' → 'complete'
});

// 方法三:若脚本已在 body 末尾或使用 defer,可直接访问 DOM
// <script src="app.js" defer></script>

// window.onload:等待所有子资源(图片/字体)加载完
window.addEventListener('load', function() {
    var img = document.querySelector('img');
    console.log('图片自然尺寸:', img.naturalWidth, img.naturalHeight);
});

脚本执行 子资源(图/CSS) DOM 树 HTML 解析 脚本执行 子资源(图/CSS) DOM 树 HTML 解析 readyState = 'loading' readyState = 'interactive' readyState = 'complete' 边解析边建树 解析完成 触发 DOMContentLoaded 所有资源加载完 触发 window.load

工程建议:

  • 脚本放 <body> 末尾 或使用 defer → 不阻塞解析,无需等 DOMContentLoaded
  • 需要图片尺寸等子资源信息 → 用 window.load
  • 动态注入脚本要操作 DOM → 判断 document.readyState !== 'loading' 后再操作,否则监听事件

3. BOM完整体系与浏览器架构

3.1 BOM规范背景与实现差异

BOM(Browser Object Model)最初由Netscape Navigator 2引入,是非W3C标准的浏览器接口集合。由于历史原因,各浏览器实现存在差异,HTML5规范逐步统一了部分BOM API。

核心特征:

  1. 非标准化:长期缺乏官方规范,依赖事实标准
  2. 浏览器依赖:功能紧密耦合特定浏览器能力
  3. 安全限制:受同源策略、沙箱机制约束
  4. 异步模型:基于事件循环的异步交互模式

3.2 浏览器多进程架构与BOM

BOM执行上下文
浏览器进程架构
IPC通信
IPC通信
IPC通信
IPC通信
Browser Process

主进程

管理UI/存储/网络
Renderer Process

渲染进程

执行JS/渲染DOM
GPU Process

GPU进程

图形渲染
Plugin Process

插件进程

Flash/PDF等
Utility Process

工具进程

音频/视频等
Window对象

全局上下文
BOM APIs

专用接口
Event Loop

事件循环

进程隔离模型:

  • Chrome/Edge:多进程架构,每个标签页独立Renderer进程
  • Firefox: Electrolysis (e10s) 多进程架构
  • Safari:WebKit多进程架构
  • 隔离优势:崩溃隔离、安全沙箱、并行渲染

3.3 BOM对象层次结构图

window对象 - 顶层
document对象 - DOM树
html
head
body
meta/title/link等
div/p/span等
document

DOM入口
history

历史记录
location

地址信息
navigator

浏览器信息
screen

屏幕信息
frames

框架集合

BOM与DOM关系图

JavaScript运行环境
BOM

浏览器对象模型
DOM

文档对象模型
操作浏览器窗口

历史记录/地址栏/屏幕
操作页面内容

元素/样式/事件
Window/History

Location/Navigator/Screen
Document/Element

Node/Event
业务场景:

页面跳转/刷新控制

窗口管理/设备检测
业务场景:

动态内容/交互效果

表单处理/数据展示

3.4 Window对象:浏览器窗口的抽象

3.4.1 Window对象的双重身份

Window对象在JavaScript运行时中具有双重身份:

  1. ECMAScript Global Object:作为全局作用域,存储全局变量和内置函数
  2. BOM Window Interface:作为浏览器窗口接口,提供窗口操作能力

BOM Window职责
Global Object职责
Window对象
Global Object身份

ECMAScript规范
BOM Window接口

HTML规范
全局变量存储
内置构造函数
全局函数
窗口管理
定时器
导航控制
弹窗对话框

3.4.2 同步对话框机制(Modal Dialogs)

同步对话框会阻塞主线程,暂停JavaScript执行和页面渲染,直到用户响应。这种机制在现代Web开发中已被弃用,原因包括:

方法 返回值 阻塞特性 弃用状态
alert() undefined 完全阻塞 ⚠️ 调试用途
confirm() boolean 完全阻塞 ⚠️ 避免使用
prompt() `string null` 完全阻塞

技术原理:
用户 浏览器UI JavaScript主线程 用户 浏览器UI JavaScript主线程 alert('message') 暂停执行 阻塞事件循环 显示模态对话框 点击确定 返回undefined 恢复执行

现代替代方案:

  • alert替代:自定义Toast/Notification组件
  • confirm替代:自定义Modal/Dialog组件
  • prompt替代:自定义Form/Input组件
javascript 复制代码
var res1 = alert('警告!');
console.log(res1);

代码解释: alert()会阻塞代码执行,直到用户点击确定,返回值是undefined,常用于调试和重要提示。


javascript 复制代码
var res2 = confirm('你确定吗?');
console.log(res2);

代码解释: confirm()返回布尔值,点击确定返回true,点击取消返回false,常用于删除操作前的二次确认。


javascript 复制代码
var res3 = prompt('请输入您的银行卡密码:');
console.log(res3);

代码解释: prompt()返回用户输入的内容(字符串),点击取消返回null。由于样式不可控且阻塞执行,实际开发中很少使用。


② 窗口打开与关闭
javascript 复制代码
open();
open('网页地址');
open('网页地址', '窗口名称');
open('网页地址', '', 'width=400,height=300');
close();

代码解释: open()和close()用于窗口管理。

  • open():打开新的空白浏览器窗口
  • open('网页地址'):在新窗口中打开指定网页
  • open('网页地址', '窗口名称'):在指定窗口或iframe中打开
  • open('网页地址', '', 'width=400,height=300'):设置窗口尺寸
  • close():关闭当前窗口,但只能关闭由JavaScript打开的窗口(安全限制)

完整示例:窗口操作控制台
html 复制代码
<button id="btn01">打开空白窗口</button>
<button id="btn02">打开新窗口指定页面地址</button>
<button id="btn03">指定窗口打开网页</button>
<button id="btn04">打开新窗口指定尺寸</button>
<br><br>
<button id="btn05">双击关闭窗口</button>

<hr>

<iframe src="" frameborder="1" name="dalao" width="1000" height="400"></iframe>

<script>
    console.log(name);
    name = 'xiaole';

    var btn01 = document.getElementById('btn01');
    btn01.onclick = function() {
        open();
    };

    var btn02 = document.getElementById('btn02');
    btn02.onclick = function() {
        open('./02-打开关闭窗口.html');
    };

    var btn03 = document.getElementById('btn03');
    btn03.onclick = function() {
        open('http://www.taobao.com', 'dalao');
    };

    var btn04 = document.getElementById('btn04');
    btn04.onclick = function() {
        open('http://www.taobao.com', '', 'width=400,height=300');
    };

    var btn05 = document.getElementById('btn05');
    btn05.ondblclick = function() {
        close();
    };
</script>

代码解释: 完整的窗口操作示例。

  • name属性:可读写,用于标识窗口
  • open():打开新的空白标签页
  • open('地址'):在新窗口打开指定页面
  • open('地址', 'dalao'):在name为'dalao'的iframe中打开
  • open('地址', '', 'width=400,height=300'):设置窗口尺寸
  • close():双击关闭窗口,只能关闭由open()打开的窗口

③ 页面滚动控制
方法 说明 参数类型
scrollTo(x, y) 滚动到指定坐标 绝对位置
scrollBy(x, y) 滚动指定距离 相对距离
javascript 复制代码
scrollTo(0, 0);

scrollTo({
    left: 0,
    top: 0,
    behavior: 'smooth'
});

代码解释: scrollTo()是绝对滚动,直接跳转到指定坐标。

  • 两个参数形式:scrollTo(x, y),滚动到指定坐标
  • 对象参数形式:支持behavior属性,可选'auto'(默认,瞬间跳转)或'smooth'(平滑滚动)
  • 适合"返回顶部"等功能

javascript 复制代码
scrollBy(100, 100);

scrollBy({
    top: 600,
    behavior: "smooth"
});

代码解释: scrollBy()是相对滚动,基于当前位置进行偏移。

  • 两个参数形式:scrollBy(x, y),从当前位置向右/下滚动指定像素
  • 对象参数形式:可设置left和top,支持behavior: 'smooth'平滑滚动
  • 适合"加载更多"等功能

完整示例:页面滚动控制
html 复制代码
<style>
    .section {
        height: 500px;
        border-bottom: 2px dashed #999;
    }

    p {
        width: 2000px;
    }

    .nav {
        position: fixed;
        right: 10px;
        bottom: 100px;
        width: 100px;
    }
</style>

<h2>第一部分</h2>
<div class="section"></div>
<h2>第二部分</h2>
<div class="section">
    <p>朋亡秦由吞评揽种仁国方宋念智...</p>
</div>
<h2>第三部分</h2>
<div class="section"></div>
<h2>第四部分</h2>
<div class="section"></div>
<h2>第五部分</h2>
<div class="section"></div>
<h2>第六部分</h2>

<div class="nav">
    <button id="btn01">scrollTo</button>
    <button id="btn02">返回页面顶部</button>
    <button id="btn03">scrollBy</button>
</div>

<script>
    var btn01 = document.getElementById('btn01');
    btn01.onclick = function() {
        scrollTo(400, 200);
    };

    var btn02 = document.getElementById('btn02');
    btn02.onclick = function() {
        scrollTo({
            left: 0,
            top: 0,
            behavior: 'smooth'
        });
    };

    var btn03 = document.getElementById('btn03');
    btn03.onclick = function() {
        scrollBy({
            top: 600,
            behavior: "smooth"
        });
    };
</script>

代码解释: 完整的页面滚动示例。

  • CSS:.section高度500px,p宽度2000px使页面可水平滚动,.nav固定在右下角
  • scrollTo(400, 200):瞬间滚动到指定坐标
  • scrollTo({top: 0, behavior: 'smooth'}):平滑滚动回页面顶部
  • scrollBy({top: 600, behavior: 'smooth'}):从当前位置向下平滑滚动600px

④ 定时器机制与事件循环

定时器是JavaScript异步编程的核心基础设施,其实现基于事件循环(Event Loop)机制。

4.1 定时器在事件循环中的位置

定时器生命周期
事件循环机制
调用
Event Loop
优先执行
Call Stack

执行栈
Web APIs

定时器/DOM/AJAX
Task Queue

任务队列
Microtask Queue

微任务队列
setTimeout/setInterval

注册定时器
浏览器计时

Web APIs处理
任务入队

Timer Task
执行回调

Call Stack

4.2 定时器精度与限制
浏览器 最小延迟 延迟漂移 嵌套限制
Chrome/Edge 4ms (第5次+ ) 存在 5层后强制4ms
Firefox 4ms (第5次+ ) 存在 无强制限制
Safari 4ms (第5次+ ) 存在 无强制限制

精度限制因素:

  1. 浏览器最小粒度:现代浏览器强制最小4ms延迟(第5次嵌套开始)
  2. 主线程阻塞:同步任务阻塞会影响定时器触发时机
  3. 时间片调度:操作系统进程调度引入额外延迟
4.3 定时器类型对比
特性 setTimeout setInterval
执行模式 单次延迟执行 周性重复执行
时间保证 相对准确 存在累积误差
内存风险 低(自动清理) 高(需手动清理)
推荐场景 延迟操作、防抖 定时轮询、倒计时
最佳实践 递归调用代替setInterval 严格管理生命周期

多次定时器:

javascript 复制代码
setInterval(回调函数, 时间间隔);
clearInterval(定时器标记);

单次定时器:

javascript 复制代码
setTimeout(回调函数, 时间间隔);
clearTimeout(定时器标记);

代码解释:

  • setInterval():每隔指定时间执行一次,时间间隔单位是毫秒,返回定时器标记用于清除
  • clearInterval():通过定时器标记清除指定的定时器
  • setTimeout():延迟指定时间后执行一次,返回定时器标记用于取消
  • clearTimeout():在定时器执行前取消它

完整示例1:多次定时器
html 复制代码
<button id="btn">停止定时器</button>
<div id="box">10</div>

<style>
    #box {
        font-size: 200px;
    }
</style>

<script>
    var intervalId01 = setInterval(function(num) {
        console.log('hello 高小乐', Math.random(), num);
    }, 1000, Math.random());

    var intervalId02 = setInterval(randBgColor, 50);
    function randBgColor() {
        var r = Math.floor(Math.random() * 256);
        var g = Math.floor(Math.random() * 256);
        var b = Math.floor(Math.random() * 256);
        document.body.style.backgroundColor = 'rgb(' + r + ',' + g + ',' + b + ')';
    }

    var num = 10;
    var boxEle = document.getElementById('box');

    var intervalId03 = setInterval(function() {
        num--;
        boxEle.innerHTML = num;

        if (num === 0) {
            clearInterval(intervalId03);
        }
    }, 1000);

    var btn = document.getElementById('btn');
    btn.onclick = function() {
        clearInterval(intervalId02);
    };
</script>

代码解释: setInterval()的三种用法:

  1. 带额外参数:第三个参数及之后的参数会传递给回调函数,每秒执行一次输出随机值
  2. 随机背景色 :每50毫秒生成随机RGB值并设置背景色,产生闪烁效果。Math.random()生成0-1的随机数,乘256取整得到0-255的随机值
  3. 倒计时:每1000毫秒(1秒)执行一次,数字递减,倒计时结束时必须清除定时器,否则会一直执行产生负数
  4. 停止定时器 :点击按钮调用clearInterval()停止指定的定时器

完整示例2:单次定时器
html 复制代码
<button id="btn">清除单次定时器</button>
<div id="box">10</div>

<style>
    #box {
        font-size: 400px;
    }
</style>

<script>
    var timeId = setTimeout(function() {
        console.log('啊,我执行了!');
    }, 4000);

    var btn = document.getElementById('btn');
    btn.onclick = function() {
        clearTimeout(timeId);
    };

    var num = 11;
    var box = document.getElementById('box');

    function runTime() {
        num--;
        box.innerHTML = num;

        if (num === 0) {
            return;
        }

        setTimeout(runTime, 1000);
    }

    runTime();
</script>

代码解释: 单次定时器setTimeout()的使用:

  1. 基础用法:4秒后执行回调函数,只执行一次。在4秒内点击按钮可清除定时器
  2. 递归倒计时(推荐) :通过递归调用setTimeout实现周期执行效果。倒计时结束时直接返回不再递归。相比setInterval,递归方式可以避免时间漂移问题(时间累积误差)

⑤ Window对象属性总结
属性/方法 说明 示例值
name 窗口名字,可读写 'xiaole'
innerWidth 视口宽度(像素) 1920
innerHeight 视口高度(像素) 1080
document 文档对象(DOM入口) #document
history 历史记录对象 History {...}
location 地址信息对象 Location {...}
navigator 浏览器信息对象 Navigator {...}
screen 屏幕信息对象 Screen {...}
javascript 复制代码
console.log(innerWidth, innerHeight);

代码解释: innerWidth和innerHeight获取浏览器视口(可见区域)的宽度和高度,不含浏览器边框和工具栏,是响应式设计、动态布局、动画计算的重要参数。


3.5 History 对象

History表示本窗口的历史记录,提供页面导航能力,但只能访问当前窗口的历史记录,无法获取其他窗口的历史记录。

属性/方法 说明 参数
length 获取历史记录数量 无
back() 回到历史记录上一个 无
forward() 回到历史记录下一个 无
go(n) 前进或后退n步 数字(正负数)
javascript 复制代码
console.log('历史记录的数量:', history.length);

history.back();

history.go(-2);

history.forward();

history.go(2);

代码解释: history对象模拟浏览器的前进后退功能:

  • length:获取历史记录数量
  • back():后退一步,等同于点击浏览器后退按钮,等同于history.go(-1)
  • go(-2):一次后退两页
  • forward():前进一步,等同于点击浏览器前进按钮,等同于history.go(1)
  • go(2):一次前进两页

完整示例:历史记录导航
html 复制代码
<button id="btn01">后退一步</button>
<button id="btn02">后退两步</button>
<button id="btn03">前进一步</button>
<button id="btn04">前进两步</button>

<script>
    console.log(history);
    console.log('历史记录的数量:', history.length);

    var btn01 = document.getElementById('btn01');
    btn01.onclick = function() {
        history.back();
    };

    var btn02 = document.getElementById('btn02');
    btn02.onclick = function() {
        history.go(-2);
    };

    var btn03 = document.getElementById('btn03');
    btn03.onclick = function() {
        history.forward();
    };

    var btn04 = document.getElementById('btn04');
    btn04.onclick = function() {
        history.go(2);
    };
</script>

代码解释: 完整的历史记录导航示例。假设用户访问历史为01.html → 百度 → 01.html → 尚硅谷 → 01.html(当前页),history.length = 5:

  • 后退一步:回到上一页(尚硅谷)
  • 后退两步:回到两页前(百度)
  • 前进一步:前进到下一页(如果已经是最新页面则无效)
  • 前进两页:前进两页(如果历史记录不足则无效)

3.5.1 History API 与 SPA 路由

HTML5 引入的 History API 使单页应用(SPA)能在不整页刷新 的前提下修改 URL,并与浏览器前进/后退栈协同。这是现代前端路由(Vue Router history 模式、React Router 等)的底层基础。

方法 作用 是否触发页面加载 历史栈
pushState(state, title, url) 压入新历史记录 否 新增一条
replaceState(state, title, url) 替换当前记录 否 不增加
popstate 事件 用户点击前进/后退时触发 否 读取 event.state

SPA 路由流程
hash
history
用户点击链接
路由模式
修改 location.hash
history.pushState
hashchange 事件
根据 pathname 渲染视图
用户点击后退
popstate 事件

javascript 复制代码
// 最小 history 路由示例
function navigate(path) {
    history.pushState({ path: path }, '', path);
    renderView(path);
}

window.addEventListener('popstate', function(e) {
    var path = (e.state && e.state.path) || location.pathname;
    renderView(path);
});

function renderView(path) {
    document.getElementById('app').textContent = '当前路由:' + path;
}

document.getElementById('link-about').addEventListener('click', function(e) {
    e.preventDefault();
    navigate('/about');
});

与 location.hash 路由对比:

维度 Hash 路由 (#/path) History 路由 (/path)
兼容性 IE8+ IE10+(需服务端 fallback)
SEO 较差(hash 不参与部分爬虫) 友好(配合 SSR/预渲染)
服务端配置 无需 需将所有路径回退到 index.html
锚点冲突 hash 兼作路由与锚点 无冲突

state 对象限制 :pushState 的 state 会被结构化克隆 存储,不能保存 DOM 节点或函数;刷新页面后 state 可能丢失,生产环境常结合服务端数据或 sessionStorage 恢复。


3.6 Location 对象

Location表示本窗口的地址信息,提供URL解析、修改和页面跳转能力。

相关推荐
Sarvartha37 分钟前
final 关键字
java·开发语言
程序员Sunday1 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
Setsuna_F_Seiei1 小时前
前端转型 Agent 开发 06 之 Agent Memory 记忆系统(让 Agent 更智能,更懂你)
前端·agent·ai编程
zhangzeyuaaa2 小时前
Ruby 多线程、GVL(GIL)与 Mutex 完全指南
开发语言·前端·ruby
默_笙3 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
优选资讯3 小时前
标签打印软件怎么对接 Excel 数据
前端·excel
谢亮_vipxieliang3 小时前
用 PHP 构建轻量级 REST API:从路由到鉴权的完整实践
开发语言·后端·php
思无邪663 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能
一个风轻云淡4 小时前
GCC 和 GDB命令简单解读
java·linux·前端
H.莓飛4 小时前
【数据结构】二叉树_OJ题
linux·开发语言·数据结构·算法