JavaScript BOM与DOM深度解析:底层原理与工程实践
摘要:浏览器对象模型(BOM)与文档对象模型(DOM)构成了JavaScript与浏览器交互的核心抽象层。本文从ECMAScript规范出发,深入剖析BOM/DOM的底层实现机制、内存模型、事件循环架构,以及在实际工程中的最佳实践。通过对API设计模式的解构和性能优化策略的探讨,帮助开发者建立从原理到应用的完整认知体系。
关键词:JavaScript运行时、BOM/DOM架构、事件驱动模型、性能优化、前端工程化
目录
-
- [1.1 Function对象的深度剖析](#1.1 Function对象的深度剖析)
- 核心属性与方法
- this指向决策图
- [call/apply/bind 对比图](#call/apply/bind 对比图)
- 实战案例1:字符串转换工具(短横线转小驼峰)
- [1.2 Global对象与浏览器环境](#1.2 Global对象与浏览器环境)
- [1.1 Function对象的深度剖析](#1.1 Function对象的深度剖析)
-
- [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)
-
- 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 同源策略与跨窗口通信)
-
- [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 动画帧调度)
-
[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 为何容易泄漏)
-
- [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 按业务目标再归类)
附录. 工程化实践补充
- [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对象具备以下核心特征:
- 原型链特性:Function.prototype是所有函数的原型对象,包含call、apply、bind等核心方法
- 闭包机制:通过[[Environment]]内部槽实现词法作用域的持久化
- 动态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'
绑定优先级(从高到低):
- new绑定:构造函数调用优先级最高
- 显式绑定:call/apply/bind硬编码
- 隐式绑定:对象方法调用
- 默认绑定:独立函数调用
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'));
代码解释: 将短横线分隔的字符串转为小驼峰形式。执行过程:
split('-')将字符串按短横线分割成数组map()遍历数组,第一个单词全小写,后续单词首字母大写其余小写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。
核心特征:
- 非标准化:长期缺乏官方规范,依赖事实标准
- 浏览器依赖:功能紧密耦合特定浏览器能力
- 安全限制:受同源策略、沙箱机制约束
- 异步模型:基于事件循环的异步交互模式
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运行时中具有双重身份:
- ECMAScript Global Object:作为全局作用域,存储全局变量和内置函数
- 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次+ ) | 存在 | 无强制限制 |
精度限制因素:
- 浏览器最小粒度:现代浏览器强制最小4ms延迟(第5次嵌套开始)
- 主线程阻塞:同步任务阻塞会影响定时器触发时机
- 时间片调度:操作系统进程调度引入额外延迟
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()的三种用法:
- 带额外参数:第三个参数及之后的参数会传递给回调函数,每秒执行一次输出随机值
- 随机背景色 :每50毫秒生成随机RGB值并设置背景色,产生闪烁效果。
Math.random()生成0-1的随机数,乘256取整得到0-255的随机值 - 倒计时:每1000毫秒(1秒)执行一次,数字递减,倒计时结束时必须清除定时器,否则会一直执行产生负数
- 停止定时器 :点击按钮调用
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()的使用:
- 基础用法:4秒后执行回调函数,只执行一次。在4秒内点击按钮可清除定时器
- 递归倒计时(推荐) :通过递归调用
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解析、修改和页面跳转能力。