第一部分:DOM 基础概念
1. 什么是 DOM?
-
全称: Document Object Model (文档对象模型)。
-
本质: 是浏览器将 HTML 或 XML 文档解析成的一个树形结构的对象模型。
-
作用: 它提供了一套编程接口 (API),允许 JavaScript 等脚本语言动态地访问、修改、添加或删除网页的内容、结构和样式。
-
关键理解:
-
对象模型: 网页中的每个部分(元素、属性、文本、注释等)都被表示为一个对象。
-
树形结构: 这些对象按照 HTML 的嵌套层级关系组织成一棵“树”。树有根节点 (
document
)、父节点、子节点、兄弟节点。 -
接口: DOM 定义了一堆属性 (Properties) 和 方法 (Methods),让我们可以通过 JavaScript 操作这些对象。
-
2. DOM 树结构 (节点树)
-
document
节点: 是整个 DOM 树的根节点。它代表整个 HTML 文档。所有其他节点都是它的后代。 -
元素节点 (Element Nodes): 对应 HTML 中的标签 (
<html>
,<div>
,<p>
,<img>
,<a>
等)。它们是树的主要分支。 -
属性节点 (Attribute Nodes): 依附于元素节点,表示元素的属性 (
id
,class
,src
,href
,style
等)。(注意:在现代 DOM 操作中,属性通常作为元素节点的属性直接访问,较少单独操作属性节点) -
文本节点 (Text Nodes): 包含元素内的纯文本内容。例如
<p>
标签内的文字就是一个文本节点。 -
注释节点 (Comment Nodes): 对应 HTML 中的注释
<!-- 这是一个注释 -->
。 -
节点关系 (Relationships):
-
父节点 (Parent Node): 直接包含当前节点的节点。
<body>
是<div>
的父节点。 -
子节点 (Child Node): 被当前节点直接包含的节点。
<div>
是<body>
的子节点。 -
兄弟节点 (Sibling Node): 拥有同一个父节点的节点。两个相邻的
<p>
标签互为兄弟节点。 -
后代节点 (Descendant Node): 当前节点包含的所有层级的节点(子节点、孙节点等)。
-
祖先节点 (Ancestor Node): 包含当前节点的所有上层节点(父节点、祖父节点等,直到
document
)。
-
可视化示例:
<!DOCTYPE html>
<html>
<head>
<title>我的页面</title>
</head>
<body>
<h1 id="main-title">欢迎!</h1> <!-- 元素节点 (id 是它的属性) -->
<div class="content">
<p>这是一个段落。</p> <!-- 元素节点 -->
<p>这是另一个<span>段落</span>。</p> <!-- 元素节点,内含文本节点和另一个元素节点 -->
<img src="image.jpg" alt="图片"> <!-- 元素节点 (src 和 alt 是属性) -->
</div>
</body>
</html>
对应的简化 DOM 树:
document ├── html (Element) ├── head (Element) │ └── title (Element) │ └── "我的页面" (Text) └── body (Element) ├── h1 (Element, id="main-title") │ └── "欢迎!" (Text) └── div (Element, class="content") ├── p (Element) │ └── "这是一个段落。" (Text) ├── p (Element) │ ├── "这是另一个" (Text) │ ├── span (Element) │ │ └── "段落" (Text) │ └── "。" (Text) └── img (Element, src="image.jpg", alt="图片")
第二部分:定位元素 (获取 DOM 节点)
JavaScript 需要通过 document
对象或其子节点开始,使用特定的方法“找到”你想操作的元素节点。
1. 主要方法
-
document.getElementById('idName’)
-
作用: 根据元素的唯一
id
属性查找单个元素。 -
参数: 元素的
id
值 (字符串)。 -
返回值: 匹配到的单个元素节点对象。如果没找到,返回
null
。 -
示例:
const titleElement = document.getElementById('main-title'); // 获取上面例子中的 h1
-
-
document.querySelector('cssSelector')
-
作用: 使用 CSS 选择器语法查找匹配的第一个元素。
-
参数: 一个有效的 CSS 选择器字符串 (如
'.classname'
,'#idname'
,'div p'
,'input[type="text"]'
)。 -
返回值: 匹配到的第一个元素节点对象。如果没找到,返回
null
。 -
示例:
const firstParagraph = document.querySelector('p'); // 获取文档中第一个 <p> const contentDiv = document.querySelector('.content'); // 获取 class 为 'content' 的第一个元素 const spanInSecondP = document.querySelector('p:nth-child(2) span'); // 获取第二个 p 里的 span
-
-
document.querySelectorAll('cssSelector')
-
作用: 使用 CSS 选择器语法查找匹配的所有元素。
-
参数: 一个有效的 CSS 选择器字符串。
-
返回值: 一个
NodeList
对象。它是一个类似数组的集合,包含了所有匹配到的元素节点。如果没找到,返回一个空的NodeList
。(注意:NodeList
不是真正的 JavaScript 数组,但它通常有length
属性和可以通过索引[index]
访问元素,以及forEach
方法。如果需要数组方法,可以用Array.from(nodeList)
转换) -
示例:
const allParagraphs = document.querySelectorAll('p'); // 获取文档中所有 <p> const imagesInContent = document.querySelectorAll('.content img'); // 获取 .content 里的所有 img
-
-
document.getElementsByClassName('className')
-
作用: 根据元素的
class
属性查找所有匹配的元素。 -
参数: 元素的
class
值 (字符串)。可以包含多个类名,用空格分隔(匹配包含所有这些类的元素)。 -
返回值: 一个
HTMLCollection
对象。它也是一个类似数组的、实时 (live) 的集合,包含了所有匹配的元素节点。元素在文档中的变化(添加/删除)会反映在这个集合中。如果没找到,返回一个空的HTMLCollection
。 -
示例:
const contentElements = document.getElementsByClassName('content'); // 获取所有 class 包含 'content' 的元素
-
-
document.getElementsByTagName('tagName')
-
作用: 根据 HTML 标签名查找所有匹配的元素。
-
参数: 标签名 (字符串, 如
'div'
,'p'
,'img'
)。 -
返回值: 一个
HTMLCollection
对象 (实时集合),包含了所有匹配的元素节点。 -
示例:
const allDivs = document.getElementsByTagName('div'); // 获取所有 <div> 元素
-
2. 通过节点关系定位
一旦你获得了一个节点 (elementNode
),你可以通过它的属性访问其相关节点:
-
elementNode.parentNode
/elementNode.parentElement
: 获取父节点(通常是元素节点)。 -
elementNode.childNodes
: 获取一个包含所有直接子节点的NodeList
(包括元素节点、文本节点、注释节点等)。(注意:换行和空格也会被当作文本节点!) -
elementNode.children
: 获取一个包含所有直接子元素节点的HTMLCollection
(只包含元素节点,忽略文本/注释节点)。通常更常用。 -
elementNode.firstChild
/elementNode.firstElementChild
: 获取第一个子节点 / 第一个子元素节点。 -
elementNode.lastChild
/elementNode.lastElementChild
: 获取最后一个子节点 / 最后一个子元素节点。 -
elementNode.previousSibling
/elementNode.previousElementSibling
: 获取前一个兄弟节点 / 前一个兄弟元素节点。 -
elementNode.nextSibling
/elementNode.nextElementSibling
: 获取下一个兄弟节点 / 下一个兄弟元素节点。
示例:
const contentDiv = document.querySelector('.content');
const firstChildElement = contentDiv.firstElementChild; // 第一个子元素 (第一个 <p>)
const lastChildElement = contentDiv.lastElementChild; // 最后一个子元素 (<img>)
const parentOfContent = contentDiv.parentElement; // <body>
const secondParagraph = firstChildElement.nextElementSibling; // 第二个 <p> (假设 firstChildElement 是第一个 p)
第三部分:操作元素内容、属性和样式
1. 操作元素内容
-
elementNode.innerHTML
-
作用: 获取或设置元素内部的 HTML 字符串。设置时会解析字符串中的 HTML 标签。
-
用途: 需要插入包含 HTML 标签的内容时使用(如从服务器加载一段富文本)。⚠️ 警告: 直接使用用户输入设置
innerHTML
可能导致 XSS 跨站脚本攻击。务必对用户输入进行严格过滤和转义! -
示例:
const div = document.querySelector('div'); console.log(div.innerHTML); // 输出: "<p>这是一个段落。</p><p>这是另一个<span>段落</span>。</p><img src="image.jpg" alt="图片">" div.innerHTML = '<h2>新标题</h2><p>新内容...</p>'; // 完全替换 div 内的内容 div.innerHTML += '<p>追加的内容</p>'; // 在现有内容后追加
-
-
elementNode.textContent
-
作用: 获取或设置元素及其所有后代的纯文本内容。设置时,传入的任何字符串都会被当作纯文本处理,HTML 标签不会被解析。
-
用途: 当你只需要读写文本内容,且不希望插入或解析 HTML 标签时使用。更安全(避免 XSS)。
-
示例:
const title = document.getElementById('main-title'); console.log(title.textContent); // 输出: "欢迎!" title.textContent = '你好,世界!'; // 将 h1 的文本改为 "你好,世界!"
-
-
elementNode.innerText
-
作用: 获取或设置元素的“渲染”文本内容。它会考虑 CSS 样式(如
display: none
的元素内容不会被包含),并且会触发回流(reflow)来计算样式。结果通常与用户看到的一致。 -
区别 vs
textContent
:innerText
受 CSS 影响,textContent
直接获取 DOM 节点中的文本(包括隐藏元素的文本)。innerText
性能稍差(因为需要计算样式)。通常优先使用textContent
,除非你需要精确匹配视觉呈现。
-
2. 操作元素属性
HTML 元素的属性(id
, class
, src
, href
, title
, alt
, data-*
等)可以通过 DOM 对象的属性或方法访问。
-
直接通过 DOM 对象属性访问 (最常用):
-
DOM 元素对象通常将标准的 HTML 属性映射为其自身的属性。
-
示例:
const myImage = document.querySelector('img'); console.log(myImage.src); // 获取 src 属性值 (完整 URL) console.log(myImage.alt); // 获取 alt 属性值 myImage.src = 'new-image.png'; // 设置 src 属性 myImage.title = '新图片标题'; // 设置 title 属性 const myLink = document.querySelector('a'); console.log(myLink.href); // 获取 href (完整 URL) myLink.href = 'https://www.newexample.com'; // 设置 href
-
-
elementNode.getAttribute('attrName')
-
作用: 获取元素指定属性的值。
-
参数: 属性名 (字符串)。
-
返回值: 属性值的字符串。如果属性不存在,返回
null
。 -
用途: 获取非标准属性(如
data-*
自定义属性)的值,或者需要确保获取的是原始字符串(如href
属性直接访问会返回绝对 URL,getAttribute('href')
可能返回相对路径)。 -
示例:
const dataValue = element.getAttribute('data-user-id');
-
-
elementNode.setAttribute('attrName', 'attrValue')
-
作用: 设置元素指定属性的值。如果属性不存在,则创建它。
-
参数: 属性名 (字符串), 属性值 (字符串)。
-
示例:
element.setAttribute('data-role', 'admin');
-
-
elementNode.hasAttribute('attrName')
-
作用: 检查元素是否拥有指定的属性。
-
参数: 属性名 (字符串)。
-
返回值:
true
(存在) 或false
(不存在)。 -
示例:
if (element.hasAttribute('required')) { ... }
-
-
elementNode.removeAttribute('attrName')
-
作用: 从元素中移除指定的属性。
-
参数: 属性名 (字符串)。
-
示例:
element.removeAttribute('disabled');
-
-
操作
class
属性 (特殊且常用):-
elementNode.className
:获取或设置元素的整个class
属性的字符串值(所有类名,空格分隔)。const btn = document.querySelector('button'); btn.classList.add('btn-primary', 'btn-lg'); // 添加两个类 btn.classList.remove('btn-default'); // 移除一个类 btn.classList.toggle('active'); // 切换 'active' 类 if (btn.classList.contains('disabled')) { ... } // 检查是否包含
-
更现代、更强大的方式:
elementNode.classList
属性。它返回一个DOMTokenList
对象,提供方便的方法操作类名:-
classList.add('className1', 'className2', ...)
:添加一个或多个类名。 -
classList.remove('className1', 'className2', ...)
:移除一个或多个类名。 -
classList.toggle('className', [force])
:如果类名存在则删除它,不存在则添加它。force
是可选布尔值,true
表示强制添加,false
表示强制移除。 -
classList.contains('className')
:检查元素是否包含指定的类名,返回true
或false
。 -
classList.replace('oldClass', 'newClass')
:替换一个类名。 -
示例:
const btn = document.querySelector('button'); btn.classList.add('btn-primary', 'btn-lg'); // 添加两个类 btn.classList.remove('btn-default'); // 移除一个类 btn.classList.toggle('active'); // 切换 'active' 类 if (btn.classList.contains('disabled')) { ... } // 检查是否包含
-
-
3. 操作元素样式 (style
属性)
-
elementNode.style
属性:-
作用: 获取或设置元素的内联样式 (即写在 HTML 标签
style
属性里的样式)。 -
访问方式:
element.style.cssPropertyName
。CSS 属性名需要使用驼峰式 (camelCase) 写法(因为 JavaScript 中-
是操作符):-
CSS:
background-color
-> JS:element.style.backgroundColor
-
CSS:
font-size
-> JS:element.style.fontSize
-
CSS:
z-index
-> JS:element.style.zIndex
-
-
设置值: 值必须是字符串,并且通常需要包含单位(如
px
,em
,%
)。 -
示例:
const box = document.getElementById('myBox'); box.style.backgroundColor = 'blue'; // 设置背景色 box.style.width = '200px'; // 设置宽度 box.style.padding = '10px 15px'; // 设置内边距 console.log(box.style.color); // 获取当前内联样式的 color 值 (如果没有设置过内联样式,可能返回空字符串)
-
注意:
element.style
只能获取和设置内联样式。它无法获取通过 CSS 样式表设置的样式。要获取计算后的样式(最终应用到元素上的所有样式),需要使用window.getComputedStyle(element)
。
-
-
批量修改样式: 修改
style.cssText
属性可以一次性设置多个内联样式。element.style.cssText = 'width: 100px; height: 100px; background: red;'; // 等同于 element.setAttribute('style', 'width: 100px; height: 100px; background: red;');
-
最佳实践: 对于复杂的样式修改,通常优先考虑通过修改
class
来应用预定义在 CSS 中的样式,而不是直接操作大量style
属性。这样更易于维护,且可以利用 CSS 的层叠和继承特性。
第四部分:创建、添加、删除和替换元素
1. 创建新节点
-
document.createElement('tagName')
-
作用: 创建一个新的元素节点。
-
参数: HTML 标签名 (字符串, 如
'div'
,'p'
,'li'
)。 -
返回值: 新创建的元素节点对象。此时该元素仅存在于内存中,尚未添加到 DOM 树里。
-
示例:
const newParagraph = document.createElement('p');
-
-
document.createTextNode('text')
-
作用: 创建一个新的文本节点。
-
参数: 文本内容 (字符串)。
-
返回值: 新创建的文本节点对象。
-
示例:
const newText = document.createTextNode('这是新创建的文本内容。');
-
2. 添加节点到 DOM 树
创建好的节点需要添加到 DOM 树中的特定位置才能显示在页面上。
-
parentNode.appendChild(newNode)
-
作用: 将
newNode
添加为parentNode
的最后一个子节点。 -
参数: 要添加的节点对象。
-
示例:
const newDiv = document.createElement('div'); newDiv.textContent = '我是新的 div!'; document.body.appendChild(newDiv); // 添加到 <body> 的末尾
-
-
parentNode.insertBefore(newNode, referenceNode)
-
作用: 将
newNode
插入到parentNode
的子节点referenceNode
之前。 -
参数: 要添加的节点对象 (
newNode
), 参考节点对象 (referenceNode
)。referenceNode
必须是parentNode
的子节点。 -
示例:
const newItem = document.createElement('li'); newItem.textContent = '新列表项'; const list = document.getElementById('myList'); const firstItem = list.firstElementChild; list.insertBefore(newItem, firstItem); // 在第一个列表项之前插入新项
-
-
elementNode.insertAdjacentHTML(position, htmlString)
-
作用: 将指定的 HTML 文本字符串解析为节点,并插入到相对于
elementNode
的指定位置。非常高效便捷。 -
参数:
-
position
(字符串): 指定插入位置,必须是以下之一:-
'beforebegin'
: 在elementNode
本身之前(作为前一个兄弟节点)。 -
'afterbegin'
: 在elementNode
内部的第一个子节点之前。 -
'beforeend'
: 在elementNode
内部的最后一个子节点之后。(最常用,类似appendChild
) -
'afterend'
: 在elementNode
本身之后(作为下一个兄弟节点)。
-
-
htmlString
(字符串): 要解析并插入的 HTML 字符串。
-
-
示例:
const refElement = document.querySelector('.reference'); refElement.insertAdjacentHTML('beforebegin', '<p>插入在参考元素之前</p>'); refElement.insertAdjacentHTML('afterbegin', '<span>插入在参考元素内部的开头</span>'); refElement.insertAdjacentHTML('beforeend', '<span>插入在参考元素内部的末尾</span>'); // 常用! refElement.insertAdjacentHTML('afterend', '<p>插入在参考元素之后</p>');
-
3. 移除节点
-
parentNode.removeChild(childNode)
-
作用: 从
parentNode
的子节点列表中移除指定的childNode
。 -
参数: 要移除的子节点对象。
-
返回值: 被移除的节点。这个节点仍然存在于内存中,可以稍后添加到其他地方。
-
示例:
const list = document.getElementById('myList'); const itemToRemove = list.children[1]; // 假设移除第二个子元素 list.removeChild(itemToRemove);
-
-
elementNode.remove()
(较新,更简洁)-
作用: 直接从其父节点中移除自身。
-
示例:
itemToRemove.remove(); // 效果同上
-
4. 替换节点
-
parentNode.replaceChild(newNode, oldNode)
-
作用: 用
newNode
替换parentNode
的子节点oldNode
。 -
参数: 新节点 (
newNode
), 要被替换的旧节点 (oldNode
)。oldNode
必须是parentNode
的子节点。 -
返回值: 被替换的旧节点 (
oldNode
)。 -
示例:
const oldHeading = document.querySelector('h2'); const newHeading = document.createElement('h3'); newHeading.textContent = '新标题'; oldHeading.parentNode.replaceChild(newHeading, oldHeading);
-
第五部分:事件处理 - 响应用户交互
DOM 事件是用户或浏览器与网页交互时发生的动作(如点击、鼠标移动、键盘按键、页面加载、输入框改变等)。JavaScript 可以“监听”这些事件,并在事件发生时执行特定的代码(事件处理函数)。
1. 事件监听 (注册事件处理程序)
有三种主要方式为元素添加事件监听器:
-
HTML 属性事件处理程序 (不推荐):
<button onclick="alert('按钮被点击了!')">点我</button>
-
或调用外部函数:
<button onclick="handleClick()">点击我</button> <script> function handleClick() { alert("静态注册成功!"); } </script>
-
缺点: HTML 和 JavaScript 代码紧密耦合,难以维护,且只能添加一个处理程序。
-
-
DOM 属性事件处理程序:
const button = document.querySelector('button'); button.onclick = function() { alert('按钮被点击了!'); };
-
缺点: 一个事件类型只能绑定一个处理函数。后绑定的会覆盖前面的。
-
-
addEventListener()
(推荐!)-
语法:
elementNode.addEventListener(eventType, listenerFunction, [options])
-
参数:
-
eventType
(字符串): 事件类型名称 (如'click'
,'mouseover'
,'keydown'
,'submit'
,'change'
,'load'
)。不加on
前缀! -
listenerFunction
(函数): 当事件发生时被调用的函数。这个函数会接收到一个Event
对象作为参数,该对象包含事件相关信息。 -
options
(可选对象或布尔值):-
布尔值:表示事件是否在捕获阶段触发(默认为
false
,在冒泡阶段触发)。(见下方事件流) -
对象:常用属性
{ once: true }
(只触发一次后自动移除监听器),{ passive: true }
(向浏览器承诺处理函数不会调用preventDefault()
,用于优化滚动等性能),capture
(同布尔值)。
-
-
-
优点: 可以为同一个元素的同一个事件类型添加多个处理函数,不会覆盖。提供了更细粒度的控制(捕获/冒泡)。可以使用
removeEventListener()
精确移除。 -
示例:
const button = document.querySelector('button'); function handleClick(event) { console.log('按钮被点击了!', event); // event.target 指向实际触发事件的元素 (可能是 button 或其子元素) // event.currentTarget 指向当前正在处理事件的元素 (即绑定了监听器的 button) } button.addEventListener('click', handleClick); // 可以添加另一个处理函数 button.addEventListener('click', function() { console.log('第二个处理函数'); });
-
2. 事件对象 (Event
)
当事件处理函数被调用时,浏览器会自动传入一个 Event
对象(或其子类型对象,如 MouseEvent
, KeyboardEvent
)作为参数。这个对象包含大量与事件相关的信息:
-
常用属性和方法:
-
event.target
: 触发事件的最深层元素(事件实际发生的源头)。 -
event.currentTarget
: 绑定事件监听器的元素(即addEventListener
被调用的元素)。在处理函数内部,this
通常也指向event.currentTarget
。 -
event.type
: 事件的类型 (如'click'
)。 -
event.preventDefault()
: 阻止事件的默认行为。例如,阻止链接跳转 (<a>
的click
事件)、阻止表单提交 (<form>
的submit
事件)。 -
event.stopPropagation()
: 阻止事件冒泡。防止事件继续向父元素传播。 -
event.stopImmediatePropagation()
: 阻止事件冒泡,并且阻止同一个元素上绑定的其他同类型事件处理函数被执行。 -
鼠标事件 (
MouseEvent
) 特有:-
event.clientX
/event.clientY
: 鼠标指针相对于浏览器视口 (viewport) 的 X/Y 坐标。 -
event.pageX
/event.pageY
: 鼠标指针相对于整个文档的 X/Y 坐标(包含滚动偏移)。 -
event.button
: 被按下的鼠标按钮 (0=左键, 1=中键, 2=右键)。
-
-
键盘事件 (
KeyboardEvent
) 特有:-
event.key
: 按下的键的字符串表示 (如'a'
,'Enter'
,'ArrowUp'
,'Escape'
)。 -
event.code
: 按下的键的物理按键代码 (如'KeyA'
,'Enter'
,'ArrowUp'
,'Escape'
)。布局无关。 -
event.ctrlKey
/event.shiftKey
/event.altKey
/event.metaKey
(Mac 的 Command 键): 布尔值,表示这些修饰键是否在事件发生时被按住。
-
-
3. 事件流:捕获与冒泡 (Event Flow)
当事件发生在具有父元素的元素上时,现代浏览器的事件传播分为三个阶段:
-
捕获阶段 (Capturing Phase): 事件从
window
对象向下传播,经过document
,<html>
,<body>
,层层传递直到实际触发事件的目标元素 (target
)。(默认情况下,addEventListener
在此阶段不触发处理函数,除非将options
设为true
或{ capture: true }
) -
目标阶段 (Target Phase): 事件到达目标元素
target
本身。在此阶段,绑定在target
上的处理函数(无论是否捕获)都会触发。 -
冒泡阶段 (Bubbling Phase): 事件从目标元素
target
向上冒泡传播,经过其父元素、祖父元素,层层向上直到window
对象。(默认情况下,addEventListener
在此阶段触发处理函数)
理解冒泡: 如果点击了一个 <button>
(在 <div>
内):
-
捕获阶段:
window
->document
-><html>
-><body>
-><div>
(如果这些元素有在捕获阶段的监听器) -
目标阶段:
<button>
上的监听器触发。 -
冒泡阶段:
<div>
-><body>
-><html>
->document
->window
(如果这些元素有在冒泡阶段的监听器)
event.stopPropagation()
的作用就是阻止事件在捕获或冒泡阶段继续传播。
4. 事件委托 (Event Delegation)
利用事件冒泡机制,我们可以将事件监听器绑定在一个公共的父元素上,而不是为每个子元素单独绑定。当事件在子元素上触发并冒泡到父元素时,父元素的监听器会根据 event.target
来判断是哪个子元素触发的事件,并执行相应的操作。
优点:
-
性能优化: 减少事件监听器的数量,尤其对大量动态生成的子元素非常高效。
-
简化代码: 无需为每个新添加的子元素手动绑定事件。
-
处理动态元素: 对后来添加的子元素自动生效。
示例: 给一个 <ul>
下的所有 <li>
添加点击事件。
const list = document.getElementById('myList');
list.addEventListener('click', function(event) {
// 检查点击的目标 (event.target) 是否是 <li> 元素 (或者我们需要委托的元素)
if (event.target.tagName === 'LI') { // 注意 tagName 是大写的
console.log('你点击了列表项:', event.target.textContent);
// 在这里执行针对被点击的 <li> 的操作
}
// 如果列表项里还有子元素 (比如 <span>),event.target 可能是那个子元素。
// 如果需要精确委托到 <li>,可能需要向上遍历找到最近的 <li> 父元素
// let clickedLi = event.target.closest('li');
// if (clickedLi) { ... }
});
5. 移除事件监听器
-
removeEventListener(eventType, listenerFunction, [options])
-
作用: 移除之前使用
addEventListener()
添加的事件监听器。 -
参数: 必须与添加时使用的
eventType
、listenerFunction
函数引用和options
(如果添加时指定了)完全一致。 -
关键:
listenerFunction
必须是同一个函数对象(不能是匿名函数,除非你保存了引用)。 -
示例:
function handleClick() { console.log('Clicked'); } button.addEventListener('click', handleClick); // 稍后移除 button.removeEventListener('click', handleClick);
-
第六部分:其他重要概念和技巧
1. window.onload
vs DOMContentLoaded
-
window.onload
/window.addEventListener('load', ...)
: 当整个页面(包括 HTML、CSS、JavaScript、图片、iframe 等所有资源)完全加载完毕后触发。 -
document.addEventListener('DOMContentLoaded', ...)
: 当 HTML 文档被完全解析和加载完成(DOM 树构建完毕),无需等待样式表、图像和子框架完全加载时触发。 -
选择: 如果你的脚本只需要操作 DOM,不依赖外部资源(如图片尺寸),使用
DOMContentLoaded
可以让用户更早地与页面交互。如果需要确保所有资源(特别是图片)都加载完成,则使用load
。
2. 操作表单元素 (<input>
, <select>
, <textarea>
)
表单元素有其特定的属性和方法:
-
获取/设置值:
-
文本输入框 (
<input type="text">
,<textarea>
):element.value
-
单选/复选框 (
<input type="radio">
,<input type="checkbox">
):element.checked
(布尔值) -
下拉选择框 (
<select>
):-
element.value
: 获取选中的<option>
的value
属性。 -
element.selectedIndex
: 获取选中选项的索引。 -
element.options[element.selectedIndex].text
: 获取选中选项的显示文本。
-
-
-
监听变化: 使用
'change'
事件(值改变且元素失去焦点时触发)或'input'
事件(值每次改变时实时触发)。
3. data-*
自定义属性
-
HTML5 允许使用
data-
前缀定义自定义属性来存储私有数据。 -
访问方式:
-
elementNode.dataset.propertyName
: 通过元素的dataset
属性访问。属性名需要去掉data-
前缀,并使用驼峰式访问。-
HTML:
<div data-user-id="123" data-role="admin"></div>
-
JS:
element.dataset.userId
(值为'123'
),element.dataset.role
(值为'admin'
)
-
-
elementNode.getAttribute('data-attr-name')
: 直接获取属性值字符串。
-
4. 性能注意事项
-
减少 DOM 访问: 访问 DOM(尤其是查询)相对较慢。尽量避免在循环中重复查询相同的元素,将结果缓存到变量中。
-
批量修改: 如果需要多次修改 DOM(如添加多个子元素),尽量先将这些修改在内存中完成(使用
DocumentFragment
或临时父元素),然后一次性添加到 DOM 树中,减少回流次数。 -
事件委托: 如前所述,是优化事件处理的利器。
-
避免强制同步布局 (Forced Synchronous Layout): 连续进行读取布局属性(如
offsetWidth
,scrollTop
)和修改样式的操作时,浏览器可能会为了提供准确的读取值而进行多次不必要的布局计算。尽量将读取操作集中在一起,修改操作集中在一起。
第七部分:实战小练习
让我们用一个小例子整合所学知识:
目标: 创建一个待办事项列表 (To-Do List)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>简易待办事项</title>
<style>
#todo-list { list-style: none; padding: 0; }
#todo-list li { margin: 5px 0; padding: 5px; background: #eee; }
#todo-list li.done { text-decoration: line-through; color: #999; }
.delete-btn { float: right; cursor: pointer; }
</style>
</head>
<body>
<h1>我的待办事项</h1>
<input type="text" id="new-todo" placeholder="输入新任务...">
<button id="add-btn">添加</button>
<ul id="todo-list"></ul>
<script>
// 1. 获取必要的 DOM 元素
const newTodoInput = document.getElementById('new-todo');
const addButton = document.getElementById('add-btn');
const todoList = document.getElementById('todo-list');
// 2. 添加新待办事项的函数
function addTodoItem() {
const todoText = newTodoInput.value.trim(); // 获取输入值并去除首尾空格
if (todoText === '') return; // 如果为空则忽略
// 3. 创建新的列表项元素 (<li>)
const newTodoItem = document.createElement('li');
// 4. 创建任务文本节点
const todoTextNode = document.createTextNode(todoText);
// 5. 创建删除按钮
const deleteButton = document.createElement('span');
deleteButton.textContent = '❌';
deleteButton.className = 'delete-btn';
deleteButton.addEventListener('click', function() {
newTodoItem.remove(); // 点击删除按钮时移除该项
});
// 6. 将文本和删除按钮添加到 <li> 中
newTodoItem.appendChild(todoTextNode);
newTodoItem.appendChild(deleteButton);
// 7. 为列表项本身添加点击事件 (标记完成/未完成)
newTodoItem.addEventListener('click', function(event) {
// 避免点击删除按钮时也触发这个事件 (利用事件委托思想检查target)
if (event.target !== deleteButton) {
newTodoItem.classList.toggle('done'); // 切换 'done' 类
}
});
// 8. 将新项添加到列表的末尾
todoList.appendChild(newTodoItem);
// 9. 清空输入框并聚焦
newTodoInput.value = '';
newTodoInput.focus();
}
// 10. 为"添加"按钮绑定点击事件
addButton.addEventListener('click', addTodoItem);
// 11. 为输入框绑定回车键事件 (keydown 或 keypress)
newTodoInput.addEventListener('keydown', function(event) {
if (event.key === 'Enter') { // 按下回车键
addTodoItem();
}
});
// 12. 页面加载后让输入框自动聚焦 (可选)
window.addEventListener('DOMContentLoaded', function() {
newTodoInput.focus();
});
</script>
</body>
</html>
这个例子涵盖了:
-
获取元素 (
getElementById
) -
创建元素 (
createElement
) 和文本节点 (createTextNode
) -
操作元素内容 (
textContent
,appendChild
) -
操作类名 (
classList.add
,classList.toggle
) -
添加事件监听 (
addEventListener
):按钮点击、列表项点击、删除按钮点击、输入框回车键 -
事件对象 (
event.key
,event.target
) -
移除节点 (
remove()
) -
表单输入处理 (
value
) -
DOMContentLoaded
事件
总结
-
DOM 是什么: 理解 DOM 是浏览器创建的、代表文档结构的树形对象模型。
-
定位元素: 熟练使用
getElementById
,querySelector
,querySelectorAll
,getElementsByClassName
,getElementsByTagName
以及节点关系属性查找元素。 -
操作内容/属性/样式: 使用
innerHTML
,textContent
,style
,classList
,getAttribute
,setAttribute
等修改元素。 -
增删改节点: 使用
createElement
,createTextNode
,appendChild
,insertBefore
,insertAdjacentHTML
,removeChild
/remove()
,replaceChild
动态改变 DOM 结构。 -
事件处理: 理解事件流(捕获、目标、冒泡),使用
addEventListener
注册事件,使用Event
对象,掌握事件委托技巧,使用removeEventListener
移除事件。 -
其他要点:
window.onload
vsDOMContentLoaded
, 表单操作,data-*
属性, 性能意识。
持续学习建议:
-
实践!实践!实践! 构建小项目(如计算器、图片画廊、交互式表单验证)。
-
查阅 MDN Web 文档: 这是学习 Web 技术最权威、最详细的资源 (文档对象模型(DOM) - Web API | MDN)。
-
学习浏览器开发者工具: 使用 Elements 面板查看和修改 DOM,使用 Console 面板测试代码,使用 Event Listeners 面板调试事件。
-
探索更多 DOM API: 如
Element
的closest()
,matches()
,getBoundingClientRect()
等方法。