DOM操作与事件编程

Web开发专题 · 掌握页面交互的核心技术

专题:JavaScript与Web前端开发

关键词:DOM操作, 事件处理, addEventListener, 事件委托, BOM, JavaScript

一、DOM概述

1.1 DOM的概念

DOM(Document Object Model,文档对象模型)是浏览器将HTML文档解析为树状结构后暴露给JavaScript的编程接口。它将网页中的每个元素都映射为一个对象,开发者可以通过操作这些对象来动态修改网页的内容、结构和样式。DOM是独立于平台和语言的,W3C将其标准化为推荐规范,目前主流浏览器均实现了DOM Level 1至Level 4的核心规范。

DOM的核心思想在于:它将一个HTML文档抽象为一棵节点树(Node Tree),每个节点代表文档中的一部分(元素、文本、属性、注释等)。通过这棵树,JavaScript可以任意遍历、查询和修改文档内容,这就是网页动态交互的根本原理。

1.2 DOM树结构

当浏览器加载一个HTML页面时,会解析HTML标签并构建一棵DOM树。树根是 document 节点,其下是唯一的 html 根元素,html 元素又包含 head 和 body 两个子节点,依此类推形成层级结构。每个HTML标签对应一个元素节点(Element Node),标签内的文本对应文本节点(Text Node),属性对应属性节点(Attribute Node,在DOM4中已归入元素节点)。

例如,对于以下HTML代码:<div id="app">Hello</div>,DOM树中会创建一个 div 元素节点,其 id 属性作为该元素节点的属性保存,其下有一个文本节点内容为"Hello"。理解这种树状结构是掌握后续所有DOM操作的基础。

1.3 document对象

document 对象是DOM树的入口点,也是浏览器提供给JavaScript操作网页的核心对象。通过 document 对象,开发者可以访问页面中的所有元素、创建新节点、修改内容和样式、注册事件监听等。document 对象是 window 对象的一个属性,但由于其重要性,在全局作用域中可以直接访问。

document 对象还提供了一些重要的子对象和集合,如 document.documentElement(即 <html> 元素)、document.head、document.body、document.forms(所有表单的集合)、document.links(所有链接的集合)等。掌握这些快捷属性可以大大简化DOM操作的代码量。

1.4 浏览器渲染过程

理解浏览器的渲染过程对于优化DOM操作至关重要。大致流程如下:
① HTML解析:浏览器将HTML字节流解析为DOM树(DOM Tree);
② CSS解析:同时解析CSS,构建CSSOM树(CSS Object Model);
③ 合并渲染树:将DOM树和CSSOM树结合为Render Tree(渲染树),只包含可见元素;
④ 布局(Layout):计算每个可见元素的几何位置和大小;
⑤ 绘制(Paint):将像素绘制到屏幕上。

当JavaScript修改DOM时,浏览器可能需要重新执行布局和绘制过程,这被称为回流(Reflow)或重绘(Repaint)。频繁的DOM操作会严重影响性能,因此需要采取批量更新、脱离文档流操作、使用DocumentFragment等优化策略。

二、DOM元素选择

2.1 getElementById()

document.getElementById(id) 是最直接、最高效的单个元素选择方法。它接收一个id字符串作为参数,返回匹配的第一个元素(理论上id应在页面中唯一)。如果找不到匹配元素,返回 null。这个方法在现代浏览器中的性能极佳,因为浏览器内部会维护一个id映射表,查找时间复杂度为O(1)。

// 基本用法 const header = document.getElementById('main-header'); if (header) { header.style.color = 'blue'; }

2.2 getElementsByClassName() / getElementsByTagName()

document.getElementsByClassName(className) 根据类名选择元素,返回一个 HTMLCollection 实时集合。这意味着当DOM发生变化时,集合会自动更新。getElementsByTagName(tagName) 根据标签名选择元素,同样返回实时 HTMLCollection。两者的共同特点是返回的是"类数组"而非真正的数组,不能直接使用数组方法(如 forEach)。

一个重要的细节是:HTMLCollection 是实时的(live),这既是优点也是潜在的风险点。在循环中遍历并修改DOM时,可能会因为集合的自动更新导致意外的行为。建议在使用时将集合转换为数组:Array.from(collection) 或 [...collection]。

// 选择所有 class 为 "item" 的元素 const items = document.getElementsByClassName('item'); console.log(items.length); // 实时反映DOM状态 // 选择所有 <p> 元素 const paras = document.getElementsByTagName('p'); // 转换为真数组以便使用数组方法 const itemArray = Array.from(items); itemArray.forEach(el => console.log(el.textContent));

2.3 querySelector() / querySelectorAll()

document.querySelector(selector) 和 document.querySelectorAll(selector) 是现代DOM编程中最推荐的选择器方法。它们接受任何有效的CSS选择器字符串作为参数,querySelector 返回匹配的第一个元素,querySelectorAll 返回所有匹配元素的静态 NodeList。NodeList 不是实时集合,不会随着DOM变化而自动更新。

querySelectorAll 返回的 NodeList 拥有 forEach 方法(现代浏览器中),但仍不是真正的数组。如果需要使用 map、filter 等高级数组方法,仍需先转换为数组:[...nodeList] 或 Array.from(nodeList)。此外,NodeList 也支持 length 属性和索引访问。

// 使用querySelector const firstBtn = document.querySelector('.btn.primary'); const container = document.querySelector('#app > .content'); // 使用querySelectorAll const allLinks = document.querySelectorAll('nav a[href^="http"]'); const evenRows = document.querySelectorAll('tr:nth-child(even)'); // NodeList可以forEach allLinks.forEach(link => { link.target = '_blank'; });

2.4 选择器性能对比

从性能角度比较:getElementById 最快(O(1)时间复杂度),其次是 getElementsByClassName 和 getElementsByTagName。querySelector 和 querySelectorAll 因为需要解析CSS选择器字符串并进行匹配,性能相对较慢,但在绝大多数实际场景中差异可以忽略不计。

建议的选型原则:如果需要根据id查找,优先使用 getElementById;如果需要批量操作且对实时性有要求,使用 getElementsByClassName;在大多数场景下,推荐使用 querySelector/querySelectorAll,因为它们选择能力最强、写法最灵活、语义最清晰。微小的性能差异在实际开发中很少成为瓶颈。

三、DOM操作

3.1 创建元素与文本节点

document.createElement(tagName) 用于创建新的元素节点,参数为要创建的标签名。新创建的元素还不在DOM树中,需要通过插入方法将其添加到文档中。document.createTextNode(text) 用于创建文本节点,通常 appendChild 可以直接接受字符串(浏览器会隐式创建文本节点),但在需要精细控制时仍会用到。

// 创建元素 const div = document.createElement('div'); div.id = 'new-div'; div.className = 'box'; // 创建文本节点并追加 const text = document.createTextNode('这是内容'); div.appendChild(text);

3.2 插入元素

插入元素有多种方法,每种适用于不同的场景。appendChild(child) 将子节点追加到父节点的末尾,是经典方法。insertBefore(newNode, referenceNode) 在参考节点之前插入新节点,若 referenceNode 为 null 则等同于 appendChild。现代方法 append() 和 prepend() 更为灵活,可以同时插入多个节点或字符串。

const parent = document.getElementById('list'); const newItem = document.createElement('li'); newItem.textContent = '新项目'; // 追加到末尾 parent.appendChild(newItem); // 在第一个子元素前插入 parent.insertBefore(newItem, parent.firstChild); // 现代方法:可插入多个 parent.append('文本', document.createElement('span')); parent.prepend('前置内容');

3.3 删除元素

removeChild(child) 是传统删除方法,需要先获取父元素再调用。现代方法 remove() 更为简洁,元素可以自我删除,无需引用父节点。需要注意的是 remove() 方法在较老浏览器中不支持,但现代项目基本可以放心使用。

// 传统方式:需要父元素 const parent = document.getElementById('container'); const child = document.getElementById('to-remove'); parent.removeChild(child); // 现代方式:元素自删除 document.getElementById('to-remove').remove();

3.4 替换与克隆元素

replaceChild(newChild, oldChild) 用新节点替换现有子节点,需要父元素调用。cloneNode(deep) 用于克隆元素,deep 参数为 true 时会执行深克隆(包括子节点),false 只克隆元素自身(不包含子节点和事件监听器)。

特别注意:cloneNode 默认(或不传参数)为浅克隆。克隆的元素不会复制事件监听器(通过 addEventListener 绑定的),但内联事件属性(如 onclick="...")会被复制。此外,克隆出的元素 id 也不会自动改变,需要注意避免 id 冲突。

// 替换 const parent = document.getElementById('parent'); const newSpan = document.createElement('span'); newSpan.textContent = '替换内容'; parent.replaceChild(newSpan, parent.children[0]); // 克隆 const original = document.querySelector('.card'); const clone = original.cloneNode(true); // 深克隆 clone.id = 'card-clone'; // 避免id冲突 document.body.appendChild(clone);

3.6 innerHTML vs textContent

innerHTML 和 textContent 是读取和修改元素内容的两个主要属性,但它们的语义和行为有本质区别。innerHTML 将内容解析为HTML,可以包含标签和结构;textContent 将内容视为纯文本,会自动转义HTML标签。从安全性角度看,innerHTML 存在XSS(跨站脚本攻击)风险,如果设置的内容包含用户输入,攻击者可能注入恶意脚本。因此,当只是操作纯文本时,应始终优先使用 textContent。

从性能角度看,innerHTML 会触发浏览器的HTML解析器,同时由于它可能导致DOM子树完全重建,会带来更大的性能开销。textContent 仅修改文本节点,效率更高。当需要插入大量结构化HTML时,建议使用 createElement + appendChild 的方式替代 innerHTML,以保持事件监听器的绑定和更好的性能。

// innerHTML:解析HTML const div = document.getElementById('content'); div.innerHTML = '<strong>加粗文字</strong>'; // textContent:纯文本,自动转义 div.textContent = '<strong>不会加粗</strong>'; // 安全实践:用户输入必须用textContent const userInput = '<script>alert("xss")</script>'; // ❌ 危险 div.innerHTML = userInput; // ✅ 安全 div.textContent = userInput;

四、DOM属性与样式

4.1 获取/设置/移除属性

getAttribute(name) 读取元素的HTML属性值,返回字符串或 null。setAttribute(name, value) 设置或更新属性值。removeAttribute(name) 移除指定属性。这些方法操作的是HTML属性(attribute),与元素对象的属性(property)有时并不完全一致。例如,input元素的 value 属性在使用 getAttribute('value') 获取的是HTML中的初始值,而直接访问 element.value 获取的是当前值。

对于自定义属性(data-*),现在更推荐使用 dataset API(见4.4节),而不是 getAttribute/setAttribute。但布尔类型的属性(如 disabled、checked、readonly)仍需通过 getAttribute/setAttribute 或直接访问元素的 property。

const link = document.querySelector('a'); // 读取属性 console.log(link.getAttribute('href')); // 设置属性 link.setAttribute('target', '_blank'); link.setAttribute('rel', 'noopener noreferrer'); // 移除属性 link.removeAttribute('disabled'); // attribute vs property的区别 const input = document.querySelector('input'); input.value = '新值'; console.log(input.getAttribute('value')); // HTML中的初始值 console.log(input.value); // 当前值

4.2 classList操作

classList 是元素上的一个 DOMTokenList 对象,提供了专门操作CSS类名的API。相比直接操作 className 字符串,classList 更加安全、简洁且不易出错。主要方法包括:add(className) 添加一个或多个类;remove(className) 移除一个或多个类;toggle(className, force) 切换类(返回布尔值表示操作后类是否存在);contains(className) 检查类是否存在。

classList 还支持多个类名同时操作:element.classList.add('a', 'b', 'c')。在条件切换场景下 toggle 非常实用,而且支持第二个可选参数 force(布尔值),可以强制添加或移除而不进行切换判断。

const el = document.getElementById('menu'); // 添加类 el.classList.add('active', 'visible'); // 移除类 el.classList.remove('hidden'); // 切换类 el.classList.toggle('open'); // 带条件的切换 el.classList.toggle('expanded', isExpanded); // 检查类 if (el.classList.contains('active')) { console.log('元素处于激活状态'); } // 替代className繁琐操作 // ❌ 旧方式 el.className = el.className + ' new-class'; // ✅ 新方式 el.classList.add('new-class');

4.3 style属性与计算样式

element.style 属性是一个 CSSStyleDeclaration 对象,用于读写元素的内联样式(即HTML中 style 属性的内容)。通过 style 设置的样式优先级最高(仅次于 !important),适合需要动态改变的样式属性。注意属性名使用驼峰命名法(camelCase),如 backgroundColor 对应CSS中的 background-color。

getComputedStyle(element) 方法返回元素经过计算后的最终样式,包括内联样式、样式表中的规则和浏览器默认样式。返回的样式值是只读的,不能修改。这在需要读取元素实际渲染尺寸、颜色等参数时非常有用。

const box = document.getElementById('box'); // 设置内联样式 box.style.backgroundColor = '#3498db'; box.style.padding = '20px'; box.style.borderRadius = '8px'; box.style.cssFloat = 'left'; // float是保留字,用cssFloat // 批量设置样式(效率较低) Object.assign(box.style, { color: '#fff', fontSize: '16px', marginTop: '10px' }); // 获取计算样式 const styles = getComputedStyle(box); console.log(styles.backgroundColor); console.log(styles.fontSize); console.log(styles.width);

4.4 data-* 自定义属性

HTML5的 data-* 属性允许在元素上存储自定义数据。通过元素的 dataset 属性可以方便地访问这些数据:dataset 会将 data- 后的属性名转换为驼峰命名(如 data-user-name 对应 dataset.userName)。dataset 的值始终是字符串类型,如果需要数值或布尔值,需要手动转换。

与 setAttribute/getAttribute 操作 data-* 属性相比,dataset API 更加直观和易读。但在性能敏感的循环中,setAttribute/getAttribute 可能略快。此外,dataset 是实时映射(live),修改 dataset 会同步到 HTML 属性,反之亦然。

<div id="user" data-user-id="123" data-role="admin" data-last-login="2026-05-01"></div> <script> const user = document.getElementById('user'); // 读取data属性 console.log(user.dataset.userId); // "123" console.log(user.dataset.role); // "admin" console.log(user.dataset.lastLogin); // "2026-05-01" // 设置data属性 user.dataset.status = 'active'; // HTML中会新增: data-status="active" // 删除data属性 delete user.dataset.role; // HTML中会移除: data-role </script>

五、事件处理

5.1 事件监听:addEventListener()

addEventListener(type, listener, options) 是现代事件绑定的标准方法。第一个参数是事件类型字符串(如 'click'、'mouseover'),第二个参数是回调函数,第三个可选参数可以是一个配置对象或布尔值。相比传统 onclick 属性赋值的方式,addEventListener 可以为同一元素的同一事件绑定多个处理函数,且更灵活地控制事件行为。

第三个参数 options 对象支持以下属性:capture(是否在捕获阶段触发)、once(是否只执行一次后自动移除)、passive(是否不调用 preventDefault,用于提升滚动性能)。其中 passive 在滚动事件监听中非常有用,可以显著提高滚动的流畅度。

const btn = document.getElementById('myBtn'); // 基本用法 btn.addEventListener('click', function(event) { console.log('按钮被点击了'); }); // 箭头函数 btn.addEventListener('click', (e) => { console.log(e.target); }); // options:只执行一次,在捕获阶段触发 btn.addEventListener('click', handler, { once: true, capture: true }); // passive:优化滚动性能 document.addEventListener('touchstart', handler, { passive: true });

5.2 事件移除:removeEventListener()

removeEventListener(type, listener, options) 用于移除通过 addEventListener 绑定的事件监听。需要注意:移除时必须传入与绑定时完全相同的函数引用。如果绑定时使用了匿名函数或箭头函数,将无法移除该事件监听,因为无法获取相同的函数引用。因此,需要动态移除的事件监听,必须使用命名函数或保存函数引用。

// ✅ 可以移除:命名函数 function handleClick(e) { console.log('点击'); } btn.addEventListener('click', handleClick); btn.removeEventListener('click', handleClick); // ❌ 无法移除:匿名函数 btn.addEventListener('click', function(e) { console.log('点击'); }); btn.removeEventListener('click', function(e) { // 新的函数,不是同一个引用 console.log('点击'); }); // 移除失败!

5.3 事件对象(event)

当事件发生时,浏览器会创建一个事件对象作为参数传递给事件处理函数。不同事件类型的事件对象有所不同,但都继承自 Event 基类。常用属性和方法包括:
· type:事件类型字符串(如 'click');
· target:触发事件的原始元素(实际点击的目标);
· currentTarget:绑定事件处理器的元素(与 this 相同);
· preventDefault():阻止默认行为;
· stopPropagation():阻止事件传播;
· stopImmediatePropagation():阻止事件传播并阻止同一元素上的其他同类事件处理函数执行。

document.addEventListener('click', function(e) { console.log('事件类型:', e.type); // "click" console.log('触发元素:', e.target); // 实际点击的元素 console.log('绑定元素:', e.currentTarget); // document console.log('事件阶段:', e.eventPhase); // 1捕获 2目标 3冒泡 console.log('时间戳:', e.timeStamp); console.log('鼠标坐标:', e.clientX, e.clientY); });

5.4 常见事件类型

浏览器支持大量事件类型,可分为以下几大类别:
① 鼠标事件:click、dblclick、mousedown、mouseup、mouseover、mouseout、mousemove、mouseenter、mouseleave(后两者不冒泡);
② 键盘事件:keydown(按下任意键)、keypress(已废弃)、keyup(释放键);
③ 表单事件:submit、reset、change、input、focus、blur、focusin、focusout;
④ 文档/窗口事件:load、DOMContentLoaded、beforeunload、unload、scroll、resize、hashchange;
⑤ 触摸/移动事件:touchstart、touchmove、touchend、touchcancel。

其中 DOMContentLoaded 事件在HTML文档被完全加载和解析后触发(不需要等待样式表、图片和子框架完成加载),而 load 事件需要等待所有资源加载完成。理解两者的区别对性能优化至关重要。

5.5 事件捕获与冒泡

事件流(Event Flow)描述了事件从页面中接收的顺序,W3C标准将事件流分为三个阶段:
① 捕获阶段(Capture Phase):事件从 window 对象开始,沿着DOM树向下传播,直到到达目标元素的父节点;
② 目标阶段(Target Phase):事件到达目标元素本身;
③ 冒泡阶段(Bubble Phase):事件从目标元素开始,沿着DOM树向上传播,直到 window 对象。

默认情况下,所有事件处理都在冒泡阶段触发。可以通过 addEventListener 的第三个参数设置为 true 来让事件在捕获阶段触发。利用事件冒泡机制可以实现事件委托(下一节详述),但有时也需要阻止冒泡以避免不期望的父级事件触发。

<div id="outer"> <div id="inner"> <button id="btn">点击</button> </div> </div> <script> // 捕获阶段监听 document.getElementById('outer').addEventListener('click', () => { console.log('outer 捕获'); }, true); // 冒泡阶段监听(默认) document.getElementById('outer').addEventListener('click', () => { console.log('outer 冒泡'); }); document.getElementById('inner').addEventListener('click', () => { console.log('inner 冒泡'); }); document.getElementById('btn').addEventListener('click', (e) => { console.log('btn 目标阶段'); // e.stopPropagation(); // 取消注释可阻止冒泡 }); // 点击按钮时的输出顺序: // "outer 捕获" → "btn 目标阶段" → "inner 冒泡" → "outer 冒泡" </script>

5.6 事件委托(Event Delegation)

事件委托是利用事件冒泡机制的一种优化模式。核心思想是:不在每个子元素上单独绑定事件监听,而是将监听器绑定在共同的父元素上,通过事件对象的 target 属性判断实际触发的子元素。这带来了两大好处:
① 减少内存占用:只需一个监听器代替多个监听器;
② 动态元素支持:新添加的子元素自动继承事件处理能力,无需重新绑定。

事件委托的典型应用场景包括:列表项点击、表格行操作、动态生成的菜单按钮等。实现时需要判断 target 是否匹配目标选择器,可以使用 Element.matches(selector) 方法进行检测。

// ❌ 不好的做法:为每个列表项绑定事件 document.querySelectorAll('.list-item').forEach(item => { item.addEventListener('click', () => { console.log(item.textContent); }); }); // ✅ 事件委托:父元素统一处理 document.getElementById('item-list').addEventListener('click', (e) => { const target = e.target.closest('.list-item'); if (target) { console.log(target.dataset.id, target.textContent); } }); // 更通用的委托模式 function delegate(parent, selector, eventType, handler) { parent.addEventListener(eventType, function(e) { const target = e.target.closest(selector); if (target && parent.contains(target)) { handler.call(target, e); } }); } delegate(document.getElementById('list'), '.item', 'click', function(e) { console.log('点击了:', this.textContent); });

5.7 阻止默认行为与事件传播

preventDefault() 阻止元素的默认行为。例如:阻止链接跳转、阻止表单提交、阻止右键菜单弹出等。stopPropagation() 阻止事件在DOM树中的进一步传播(包括捕获和冒泡阶段)。stopImmediatePropagation() 更强,除了阻止传播,还会阻止当前元素上后续事件监听器的执行。

使用建议:preventDefault 是安全常用的操作,但应谨慎使用 stopPropagation。过度阻止事件传播可能破坏页面中其他依赖于事件冒泡的功能(如分析统计、全局点击处理等)。更好的做法是使用自定义事件标识或状态判断来区分事件来源。

// 阻止链接跳转 document.querySelectorAll('a[href="#"]').forEach(link => { link.addEventListener('click', e => e.preventDefault()); }); // 阻止表单提交(用于AJAX表单) document.getElementById('ajax-form').addEventListener('submit', e => { e.preventDefault(); // 通过fetch/XMLHttpRequest提交 const formData = new FormData(e.target); fetch('/api/submit', { method: 'POST', body: formData }); }); // 阻止右键菜单 document.addEventListener('contextmenu', e => { e.preventDefault(); showCustomContextMenu(e.clientX, e.clientY); });

六、BOM浏览器对象模型

6.1 window对象

BOM(Browser Object Model,浏览器对象模型)提供了与浏览器窗口交互的接口,其核心是 window 对象。window 对象在浏览器中扮演着全局对象(Global Object)的角色:所有全局变量和全局函数都是 window 的属性和方法。window 还提供了控制浏览器窗口的方法,如 open()(打开新窗口)、close()(关闭窗口)、resizeTo()(调整窗口大小)、moveTo()(移动窗口位置)、scrollTo()(滚动到指定位置)等。

常见的窗口尺寸属性包括:window.innerWidth/innerHeight(视口宽高,包含滚动条)、window.outerWidth/outerHeight(浏览器窗口总宽高)、window.screenX/screenY(窗口相对于屏幕左上角的偏移)。这些属性在实现响应式布局和弹窗定位时非常实用。

// 窗口尺寸 console.log('视口尺寸:', window.innerWidth, 'x', window.innerHeight); console.log('屏幕尺寸:', window.screen.width, 'x', window.screen.height); // 滚动 window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollBy({ top: 100, left: 0, behavior: 'smooth' }); // 定时器 const timeoutId = setTimeout(() => { console.log('2秒后执行'); }, 2000); const intervalId = setInterval(() => { console.log('每秒执行'); }, 1000); // 清除定时器 clearTimeout(timeoutId); clearInterval(intervalId); // 弹窗(谨慎使用) // alert('警告'); // const confirmed = confirm('确定吗?'); // const name = prompt('请输入姓名:');

6.2 location对象

window.location 对象(或简写为 location)提供了当前URL的详细信息以及导航控制能力。其核心属性包括:href(完整URL)、protocol(协议,如 https:)、host(主机名+端口)、hostname(主机名)、port(端口号)、pathname(路径部分)、search(查询参数,含?)、hash(哈希部分,含#)、origin(来源,只读)。

location 对象还提供了导航方法:location.assign(url) 加载新文档(可回退)、location.replace(url) 替换当前文档(不可回退)、location.reload(force) 重新加载当前页面(force为true时强制从服务器加载而非缓存)。

// 获取当前URL信息 console.log(location.href); // https://example.com/page?id=1#section console.log(location.protocol); // https: console.log(location.host); // example.com console.log(location.pathname); // /page console.log(location.search); // ?id=1 console.log(location.hash); // #section // URL参数解析 function getQueryParams() { const params = new URLSearchParams(location.search); return Object.fromEntries(params.entries()); } console.log(getQueryParams()); // { id: '1' } // 页面跳转 // location.href = 'https://other.com'; // 方式一 // location.assign('https://other.com'); // 方式二 // location.replace('https://other.com'); // 方式三:不会留下历史记录 // 页面刷新 // location.reload(); // 可能从缓存加载 // location.reload(true); // 强制从服务器加载

6.3 history对象

window.history 对象提供了对浏览器会话历史(当前标签页的访问记录)的访问能力。主要方法包括:history.back()(相当于用户点击后退按钮)、history.forward()(前进)、history.go(delta)(相对当前页面跳转指定步数,正数前进、负数后退)。history.length 属性表示当前会话历史中的页面数量。

在现代单页应用(SPA)开发中,HTML5提供了 history.pushState(state, title, url) 和 history.replaceState(state, title, url) 方法,允许在不刷新页面的情况下修改URL和浏览器历史记录。配合 popstate 事件监听,可以实现无刷新路由导航。

// 基本导航 history.back(); // 后退 → 相当于 history.go(-1) history.forward(); // 前进 → 相当于 history.go(1) history.go(-2); // 后退2页 // HTML5 History API - SPA路由核心 // pushState: 添加新历史记录 history.pushState({ page: 1 }, '标题', '/page/1'); // replaceState: 替换当前历史记录(不会增加新条目) history.replaceState({ page: 2 }, '标题', '/page/2'); // 监听历史变化 window.addEventListener('popstate', (e) => { console.log('状态变化:', e.state); // 根据e.state更新页面内容 });

6.4 navigator对象

window.navigator 对象包含了浏览器的相关信息,常用于浏览器检测、功能判断和用户代理分析。常用属性包括:userAgent(浏览器用户代理字符串)、platform(操作系统平台)、language(首选语言)、languages(语言列表)、online(是否在线)、cookieEnabled(是否启用Cookie)、geolocation(地理定位API)、clipboard(剪贴板API)等。

需要注意:userAgent 字符串可以被用户或浏览器插件修改,不应仅依赖它做关键功能判断。推荐使用特性检测(Feature Detection)而非浏览器检测(Browser Detection)。例如,通过检测 navigator.geolocation 是否存在来判断浏览器是否支持地理定位,而不是检测是否为 Chrome。

// 浏览器信息 console.log('User-Agent:', navigator.userAgent); console.log('平台:', navigator.platform); console.log('语言:', navigator.language); console.log('在线状态:', navigator.onLine); // 网络状态监听 window.addEventListener('online', () => console.log('网络已连接')); window.addEventListener('offline', () => console.log('网络已断开')); // 特性检测示例 if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( pos => console.log('纬度:', pos.coords.latitude), err => console.error('定位失败:', err.message) ); } else { console.log('该浏览器不支持地理定位'); } // 剪贴板API async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); console.log('复制成功'); } catch (err) { console.error('复制失败:', err); } }

6.5 localStorage / sessionStorage

Web Storage API 提供了在浏览器中存储键值对数据的机制,是Cookie的现代替代方案(存储容量更大,约5-10MB,且不会自动随HTTP请求发送到服务器)。两种存储方式的核心区别在于生命周期和作用域:

localStorage 持久化存储,数据不会过期,除非手动删除或用户清除浏览器数据。同一域名下所有标签页共享 localStorage 数据,且页面关闭后数据仍然保留。sessionStorage 会话级存储,数据仅在当前标签页(会话)有效,关闭标签页即被清除。即使是同域名下的不同标签页,sessionStorage 也是独立的。

两者的API完全一致:setItem(key, value)、getItem(key)、removeItem(key)、clear() 清空所有存储。需要注意的是:存储的值只能是字符串类型,存储对象或数组时需要先 JSON.stringify 序列化,读取后需要 JSON.parse 反序列化。

// localStorage - 持久存储 localStorage.setItem('theme', 'dark'); localStorage.setItem('fontSize', '16'); const theme = localStorage.getItem('theme'); console.log(theme); // "dark" localStorage.removeItem('fontSize'); // localStorage.clear(); // 慎用:清除所有 // sessionStorage - 会话存储 sessionStorage.setItem('tempData', JSON.stringify({ id: 1, name: '临时' })); const data = JSON.parse(sessionStorage.getItem('tempData')); // 存储/读取复杂数据 const user = { name: '张三', age: 28, roles: ['admin', 'editor'] }; localStorage.setItem('user', JSON.stringify(user)); const cachedUser = JSON.parse(localStorage.getItem('user')); console.log(cachedUser.name); // "张三" // storage 事件:跨标签页通信 window.addEventListener('storage', (e) => { console.log('存储变化:', e.key, e.oldValue, '→', e.newValue); // 注意:该事件只在其他标签页修改storage时触发 }); // 封装简易存储工具 const storage = { set(key, value) { localStorage.setItem(key, JSON.stringify(value)); }, get(key) { try { const val = localStorage.getItem(key); return val ? JSON.parse(val) : null; } catch { return localStorage.getItem(key); } }, remove(key) { localStorage.removeItem(key); }, clear() { localStorage.clear(); } }; storage.set('settings', { lang: 'zh-CN', volume: 0.8 }); console.log(storage.get('settings')); // { lang: 'zh-CN', volume: 0.8 }

总结与核心要点

1. DOM本质:DOM是将HTML文档映射为树状结构的编程接口,是JavaScript操作网页的基础。

2. 选择器原则:优先使用 querySelector/querySelectorAll(灵活性高),需要id定位时用 getElementById(性能最优)。

3. 操作安全:操作文本用 textContent(防XSS),操作HTML用 createElement + appendChild(保留事件绑定)。

4. 样式操作:类名操作使用 classList API,读取计算样式用 getComputedStyle,内联样式用 style 属性。

5. 事件机制:事件流分捕获→目标→冒泡三阶段,委托模式利用冒泡提升性能和动态性。

6. 性能优化:批量DOM操作使用 DocumentFragment,高频事件防抖节流,滚动监听启用 passive 选项。

7. 存储策略:持久数据用 localStorage,会话数据用 sessionStorage,涉及跨标签页通信监听 storage 事件。

8. 最佳实践:使用特性检测代替浏览器检测;事件委托代替批量绑定;addEventListener 代替 onclick 属性;removeEventListener 务必使用命名函数。