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

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

关键词:JavaScript运行时、BOM/DOM架构、事件驱动模型、性能优化、前端工程化

目录

  1. 内置对象回顾与进阶

  2. DOM核心概念

  3. BOM完整体系

  4. 实战案例详解

  5. 性能优化与最佳实践

  6. 事件循环全景:宏任务、微任务与渲染帧

  7. BOM/DOM 理论体系深化

  8. 经典业务场景与可实现的业务

  9. 总结

附录. 工程化实践补充


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


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绑定

完整示例代码
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个形参(分隔符)。


var user = {
    name: '曹操',
    getInfo: func
};

func(10, 20);

user.getInfo(4, 6);

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


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]


var res1 = func.apply('hello', [19, 2]);
console.log(res1);

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

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


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)。


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

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


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

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


var fn01 = func.bind([10, 20, 30, 40, 50]);
console.log(fn01(10, 20));

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


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:字符串转换工具(短横线转小驼峰)
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的实现。像ArrayNumberStringisNaNisFinite等都是Global的属性。

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

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


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符号保持不变。


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操作的黄金四件套

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

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

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

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


完整示例:交互式盒子

<!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() 无效。

// 推荐:可组合、可移除、支持委托
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 个监听器。

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.thenMutationObserver 等为微任务(Microtask),在当前宏任务清空后、下一个宏任务前全部执行。

用户点击

浏览器合成事件对象

宏任务:dispatch 事件

执行监听器同步代码

微任务队列清空

可能触发重排/重绘

requestAnimationFrame 回调

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


2.6 现代 Observer API

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

API 观察对象 典型场景 与 BOM/DOM 关系
IntersectionObserver 元素与视口交叉 图片懒加载、曝光埋点 替代 scroll + getBoundingClientRect
MutationObserver DOM 树变更 第三方组件监控、富文本同步 微任务回调,批量记录 mutations
ResizeObserver 元素尺寸变化 图表自适应、容器查询 window.resize 更细粒度
// 图片懒加载:进入视口前不请求真实 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 哈希表 O(1),最快
getElementsByClassName HTMLCollection ✓ Live 类名 文档变更时自动更新
getElementsByTagName HTMLCollection ✓ Live 标签名 同上
querySelector(sel) Element | null - CSS 选择器 深度优先遍历,首个匹配
querySelectorAll(sel) 静态 NodeList CSS 选择器 快照,后续 DOM 变更不影响

Live vs Static 的工程陷阱

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');
});

querySelectorgetElementById 选型

  • 已知唯一 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-routercreateWebHistory() react-routercreateBrowserRouter
列表渲染 手动 createElement v-for → patch DOM map → Virtual DOM diff
状态驱动 UI 手动改 innerHTML 响应式 → 自动 patch setState → reconcile

合成事件(React)与原生事件的区别:React 17+ 将委托挂载到根容器而非 document,事件对象在冒泡到根时统一包装为 SyntheticEvente.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 插槽

内部结构 / 样式

主文档中的子节点

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
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 的对比

// ❌ 旧写法:容易破坏已有类名
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(短横线转小驼峰)

<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>
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-*,自动驼峰转换
// 等价写法
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 | null
element.lastElementChild 最后一个子元素 Element | null
element.previousElementSibling 前一个兄弟元素 Element | null
element.nextElementSibling 后一个兄弟元素 Element | null
element.parentElement 父元素 Element | null
element.parentNode 父节点(可能是 Document) Node | null
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
// 方法一:监听 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组件
var res1 = alert('警告!');
console.log(res1);

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


var res2 = confirm('你确定吗?');
console.log(res2);

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


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

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


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

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

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

完整示例:窗口操作控制台
<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) 滚动指定距离 相对距离
scrollTo(0, 0);

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

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

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

scrollBy(100, 100);

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

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

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

完整示例:页面滚动控制
<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 严格管理生命周期

多次定时器:

setInterval(回调函数, 时间间隔);
clearInterval(定时器标记);

单次定时器:

setTimeout(回调函数, 时间间隔);
clearTimeout(定时器标记);

代码解释:

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

完整示例1:多次定时器
<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:单次定时器
<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 {...}
console.log(innerWidth, innerHeight);

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


3.5 History 对象

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

属性/方法 说明 参数
length 获取历史记录数量
back() 回到历史记录上一个
forward() 回到历史记录下一个
go(n) 前进或后退n步 数字(正负数)
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):一次前进两页

完整示例:历史记录导航
<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 事件

// 最小 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 对象限制pushStatestate 会被结构化克隆存储,不能保存 DOM 节点或函数;刷新页面后 state 可能丢失,生产环境常结合服务端数据或 sessionStorage 恢复。


3.6 Location 对象

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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐