DOM — 让 JavaScript 抓住页面
DOM — 让 JavaScript 抓住页面
这一章讲一件事:JavaScript 凭什么能改页面。 答案是个中间层:浏览器加载网页时,把 HTML 解析成一棵节点树(就是第 01、03 章反复出现的那棵 DOM 树), JavaScript 不直接碰页面,它操作这棵树——树一变,浏览器就把新样子画出来。 学了这一章,「网页」在你脑子里就该从「一张画」变成「一个可以编程的数据结构」。
1. 先看现象:一个字没变,页面却改了
一个按钮,点一下,页面顶上的标题从「Welcome to My Website」变成「Welcome to JavaScript」—— 没有刷新、没有跳转。第 11 章的语言本事都在,但它只会算和存,碰不到页面; 本章给它接上那只手:DOM。
2. 顶层全景:DOM 是什么、JavaScript 怎么用它
书里给的定义很浓缩,三个短句都要1:
- DOM 是浏览器在页面加载时创建的、HTML 文档的树状表示;
- 这棵树代表页面上 HTML 元素的层级结构;
- DOM 是页面内容与 JavaScript 之间的接口——脚本通过它动态修改结构。
HTML 文本 DOM 树(浏览器建) 你(JavaScript)
<body> body │
<h1 id="mainheading"> └── h1 元素节点 ├─ 选中:document.getElementById("mainheading")
Welcome… └── 文本节点 ├─ 修改:h.innerHTML = "Welcome to JavaScript"
<p>…</p> └── p 元素节点 ├─ 增删:document.createElement("p")
└─ 监听:h.addEventListener("click", …)
图说:四件事——选中、修改、增删、监听——就是 DOM 编程的全部动作;
任何一件做完,浏览器自动把树的最新样子画到屏幕上。两个词顺手钉死: 树上的每个点叫节点(node)——元素节点(<h1>)、文本节点(「Welcome…」那段文字)、
文档节点(document 本身,树的入口)。「选中一个元素」,正式说法是「取得对应的节点对象」2。
3. 选中:五种挑法,两个世界
改任何东西之前先选中它。书里给了五种方法,一张表分清3:
| 方法 | 挑法 | 返回 |
|---|---|---|
document.getElementById("id") | 按 id | 单个元素,找不到给 null |
document.getElementsByClassName("类名") | 按 class | HTMLCollection(一组,活的) |
document.getElementsByTagName("p") | 按标签名 | HTMLCollection(一组) |
document.querySelector("CSS选择器") | 按 CSS 选择器 | 第一个匹配,没有给 null |
document.querySelectorAll("CSS选择器") | 按 CSS 选择器 | 全部匹配(NodeList) |
getElementById 是最老也最常用的一招——id 一页唯一,所以它最多返回一个4:
// HTML: <h1 id="mainheading">Welcome to My Website</h1>
document.getElementById("mainheading");
// → 返回那个 h1 的节点对象;没有 id 为 mainheading 的元素时,返回 nullquerySelector 家族是用 CSS 选择器来挑——第 07、08 章学过的选择器全部通用:
querySelector(".highlight") 挑第一个带 highlight 类的,
querySelectorAll("ul li") 挑所有列表项。一个返回单件、一个返回集合,找不到都各自返回 null / 空集合。
null 这个返回值要提前认识:第 13 章的调试实战,就是从「getElementById 返回了 null、
代码去读 null 的属性」这个报错开始的。写选择代码时,值可能是 null,是 DOM 编程的常识前提。
4. 修改:内容、属性、样式
选中之后,改法分三路。
改内容,三个属性容易混,书里给了分界的办法5:
| 属性 | 写什么进去 | 标签会被解析吗 |
|---|---|---|
.innerHTML | HTML 源码 | 会——<b>粗体</b> 真的会变粗 |
.textContent | 纯文本 | 不会——<b> 会被当普通字符显示 |
.innerText | 纯文本 | 不会(还会顾及 CSS 隐藏,较少用) |
document.getElementById("mainheading").innerHTML = "Welcome to JavaScript";
// h1 里的内容被整体替换,页面立刻重画书的演示很干脆:一行赋值,浏览器里的标题立刻换字6。 注意是「整体替换」——innerHTML 赋值不是「往里加一点」,是把元素肚子里的东西全部换掉再装新的。
改属性:.src、.href、.id、.value、.checked、.disabled、.hidden、.title 等,
直接对节点对象赋值;表单控件的 .value 是「框里现在的字」,.checked 是「勾选没有」7。
改样式,走 .style 属性8:
document.getElementById("mainheading").style.color = "blue";.style.属性名 = 值 的写法,属性名从 CSS 的 font-size 变成驼峰 fontSize、
background-color 变成 backgroundColor(连字符在 JavaScript 里会被当成减号)。
但日常更推荐改 class 而不是改 style:classList.add("highlight") 加上一个类、
.remove 去掉、.toggle 有则去无则加——样式本身留在 CSS 文件里,JavaScript 只发「换皮肤」的指令。
好处沿用第 07 章的纪律:样式集中可查可改,脚本里不散落魔法数字。
5. 增删:让树长出新的枝
动态内容的造法,书里给了三步流水线9:
let newParagraph = document.createElement("p"); // ① 造(只在内存里,页面上还没有)
newParagraph.textContent = "This is a dynamically created paragraph."; // ② 装内容
document.body.append(newParagraph); // ③ 挂上树(此刻浏览器才画它)createElement 造出来的元素在挂树之前是「孤儿」——存在,但不在页面上。
append/appendChild 把它塞进某个父节点,挂载的一刻,它出现在屏幕上;
remove() 把自己从树上摘下来,它立刻消失。增删的本质就是树上的嫁接与剪枝。
6. 监听:页面「听得见」的秘诀
改页面是一半,另一半是听用户——点击、按键、滚动、提交。机制叫事件处理(event handling): 浏览器里发生的动作叫事件(event,click、keydown、scroll……),你的代码叫监听器(listener), 「某事件在某元素上发生时,执行我这段代码」。
事件监听器的定义(书里原话级别的重要):一个等待事件发生、触发时执行指定动作的函数; 事件发生时,JavaScript 会把这个事件的事件对象传给监听器,里面有事件的详情—— 目标元素、事件类型、鼠标位置、按下的键10。
挂载的标准做法是一个方法:addEventListener11:
element.addEventListener(eventType, callbackFunction, useCapture);
// 事件名 触发时执行的函数 (可选,默认 false)
const button = document.getElementById("myButton");
button.addEventListener("click", function (event) {
alert("Button clicked!");
console.log(event); // 事件对象:里面有 target、type、坐标……
});第二个参数叫回调函数(callback):「不是现在执行,等事件发生了浏览器替你调用」。 这里又一次见到第 11 章的函数——函数作为「可以传给别人、事后调用」的值,是这一章暗中的主角。
常用事件菜单(书里的分类表12):鼠标(click、dblclick、mouseover、mouseout)、
键盘(keydown、keyup)、表单(submit、change、focus、blur)、窗口(resize、scroll、load、unload)。
见到名字基本能猜到意思;load 值得记住——「页面全部加载完」时触发,和脚本摆放规矩(第 11 章 defer)
解决的是同一个时序问题。
主线例子:一个按钮的完整回路
本章的主线例子,是「点按钮 → 弹提示 → 控制台看事件对象」的全过程,每一步旁边是具体状态:
页面: <button id="myButton">Click me!</button>
脚本: const button = document.getElementById("myButton");
→ button = 那个按钮的节点对象(若 id 写错,这里是 null,下一步就崩)
button.addEventListener("click", function (event) { … })
→ 监听器挂上树,浏览器记住「这个按钮有人等 click」
用户: 点击按钮
→ 浏览器造一个事件对象:event.type = "click",event.target = 按钮节点,
还有坐标、按键……
→ 调用回调函数,event 作为参数传进去
→ alert("Button clicked!") 弹出;console.log(event) 在控制台打印事件对象
图说:四步一条链——选中 → 挂监听 → 事件发生 → 回调执行;第 13 章的待办应用就是这条链的加长版。7. 事件委托与冒泡:动态元素的管家
第 13 章的待办清单有个工程难题:任务是一条条动态加进来的,每条带两个按钮(Done/Remove)。 给每个按钮单独挂监听器?新加的任务就没有监听器了——总不能加一条任务挂一次。
这一行的标准解法书里给了名字:事件委托(event delegation)—— 把监听器挂在父元素上,用事件冒泡来接收所有子元素的事件13。要解释一下这两个新词:
冒泡(bubbling):事件在目标元素上发生后,会沿 DOM 树一路向上「冒」——
从按钮到它的 <li>、到 <ul>、到 body……每一层都路过一次。默认行为沿这条链走,
所以挂在祖先上的监听器,子孙的事件都能收到。冒泡还有个反向阶段(捕获,capturing),
书里点了名:传播有两个阶段,默认是冒泡阶段14。
委托的写法:监听父元素,回调里看 event.target(事件真正发生在谁身上)来决定干什么:
taskList.addEventListener("click", function (e) {
if (e.target.classList.contains("done")) {
toggleTask(e.target.parentElement.parentElement); // 从按钮向上两格找到 <li>
} else if (e.target.classList.contains("remove")) {
deleteTask(e.target.parentElement.parentElement);
}
});e.target.parentElement.parentElement 这段「向上爬两级」是 DOM 树的亲戚导航——
节点之间能互相找:.parentElement 父、.children 子、.nextElementSibling 下一个兄弟。
委托的收益书里说得直接:性能更好,因为只需要一个监听器;而且动态加进来的子元素自动被覆盖15——
新 <li> 的按钮不用挂任何监听,点击照样冒泡到父级被处理。
配套还有两个小工具:e.preventDefault() 阻止元素的默认动作(比如阻止表单真的提交跳转)16;
removeEventListener 摘除监听器(记得摘的时候传同一个函数引用)。
8. 作者的判断与证据
书内作为事实给出的: DOM 的定义与接口身份1、五种选择方法与返回值3、 innerHTML/textContent 的分界5、createElement 三步流水9、addEventListener 三参数与事件对象1011、 冒泡/捕获两阶段14、事件委托的性能理由15、preventDefault16。
作者的取舍: 这章是全书「接口设计」味最浓的一章——DOM 这个 API 是 1990 年代末定的, 如今看有不少古怪(如 getElementsByClassName 返回「活的」集合、querySelector 返回静态的), 书没展开这些坑,只教安全的默认用法(用 getElementById/querySelector)。这是合理的减法; 真写复杂交互时,再回来研究 NodeList 与 HTMLCollection 的差别不迟。
两处补充(④ 类通用知识,书里没讲):
其一,innerHTML 赋值是解析 HTML 的,拼接用户输入进去有安全隐患(XSS 注入的入门案例);
显示用户打的字,用 textContent 更安全。
其二,document.readyState(loading/interactive/complete)可以查文档加载到哪个阶段17,
和 defer/DOMContentLoaded 是同一族时序工具——第 13 章用了 DOMContentLoaded 事件。
9. 边界与局限
- 事件只教了 click 的实际用法,键盘、表单事件的实战(第 06 章表单的 JS 校验)没串起来; 本书在「表单 + JS」这个真实高频场景上留了个空。
fetch、定时器(setTimeout/setInterval)未涉及——异步交互的大门开在本书之外。innerTextvstextContent的细节(性能与渲染依赖)书里只给了概念分界。setAttribute/自定义data-*属性只在目标清单里露名18,正文未展开——第 13 章也没有用到,照实标为缺口。
10. 可带走的
全章主线例子,一行写完: getElementById 拿到按钮 → addEventListener 挂上 click 回调 → 用户点击 → 事件对象进回调 → alert 弹出、控制台打印 event。
- DOM = 浏览器把 HTML 解析成的节点树,是页面和 JavaScript 之间的接口1;
- 选中五法:ById(单个)、ByClassName/ByTagName(集合)、querySelector(第一个)/All(全部,CSS 选择器语法);
- 找不到会返回 null / 空集合——对 null 取属性是 DOM 第一大报错源;
- 改内容:innerHTML 解析标签、textContent 只当纯文本;改样式用
.style.驼峰属性,更好的是改 classList; - 增删三步:createElement(内存里造)→ 装内容 → append 挂树;remove 摘树;
- addEventListener(事件名, 回调):回调「事发了才执行」;事件对象带 target/type/坐标11;
- 事件委托 = 监听父级 + 靠冒泡 + 看 e.target;动态元素自动被覆盖,还省监听器15;
- 传播两阶段:捕获(外→内)与冒泡(内→外),默认冒泡14;
- preventDefault 拦默认动作;readyState/DOMContentLoaded 管「树建好没有」的时序。
11. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| DOM 的定义与接口身份 | Working with the DOM | text/18-fm-introduction.txt:39(搜「tree-like representation」) |
| 五种选择方法与返回值 | Working with the DOM | text/18-fm-introduction.txt:315(搜「getElementById」) · text/18-fm-introduction.txt:341(搜「returns a single element or null」) |
| getElementById 的 null 语义 | Working with the DOM | text/18-fm-introduction.txt:365(搜「or null if none is found」) |
| innerHTML 改内容 | Working with the DOM | text/18-fm-introduction.txt:367(搜「innerHTML property」) |
| textContent / innerText 分界 | Working with the DOM | text/18-fm-introduction.txt:411(搜「textContent」) · text/18-fm-introduction.txt:419(搜「respects hidden elements」) |
| 改样式与 classList | Working with the DOM | text/18-fm-introduction.txt:501(搜「style.color = "blue"」) · text/18-fm-introduction.txt:435(搜「classList」) |
| createElement 三步流水 | Working with the DOM | text/18-fm-introduction.txt:625(搜「createElement()」) |
| 事件监听器与事件对象 | Working with the DOM | text/18-fm-introduction.txt:747(搜「function that waits」) |
| addEventListener 三参数 | Working with the DOM | text/18-fm-introduction.txt:751(搜「addEventListener(eventType」) |
| 事件类型表 | Working with the DOM | text/18-fm-introduction.txt:653(搜「Mouse events」) |
| 冒泡与捕获两阶段 | Working with the DOM | text/18-fm-introduction.txt:807(搜「bubbling phase」) |
| 事件委托的性能理由 | Working with the DOM | text/18-fm-introduction.txt:805(搜「single event listener to a parent」) |
| preventDefault | Working with the DOM | text/18-fm-introduction.txt:809(搜「stops the default action」) |
| readyState | Working with the DOM | text/18-fm-introduction.txt:815(搜「readyState」) |
| 章末小结:四件事总览 | Working with the DOM | text/18-fm-introduction.txt:785(搜「tree-like representation」) |
Footnotes
-
出处:「Working with the DOM」第 39 段(
text/18-fm-introduction.txt:39,搜「tree-like representation」)。原文:「The DOM is a tree-like representation of an HTML document created by the web browser when a web page loads... The DOM acts as an interface between the page's content and JavaScript.」 ↩ ↩2 ↩3 -
出处:「Working with the DOM」第 287 段(
text/18-fm-introduction.txt:287,搜「retrieve the corresponding node object」)。原文:「you need to select it (retrieve the corresponding node object).」 ↩ -
出处:「Working with the DOM」第 307–353 段(
text/18-fm-introduction.txt:315,搜「getElementById」;:325,搜「getElementsByClassName」;:331,搜「getElementsByTagName」;:339,搜「querySelector」;:347,搜「querySelectorAll」)。五方法的返回类型表出自这一段组。 ↩ ↩2 -
出处:「Working with the DOM」第 357–365 段(
text/18-fm-introduction.txt:363,搜「getElementById("mainheading")」;:365,搜「or null if none is found」)。 ↩ -
出处:「Working with the DOM」第 405–423 段(
text/18-fm-introduction.txt:405,搜「innerHTML」;:411,搜「textContent」;:417,搜「innerText」)。 ↩ ↩2 -
出处:「Working with the DOM」第 367–395 段(
text/18-fm-introduction.txt:369,搜「Welcome to JavaScript」)。 ↩ -
出处:「Working with the DOM」第 423–491 段(
text/18-fm-introduction.txt:449,搜「value」;:473,搜「checked」;:461,搜「disabled」)。 ↩ -
出处:「Working with the DOM」第 497–619 段(
text/18-fm-introduction.txt:501,搜「style.color = "blue"」)。 ↩ -
出处:「Working with the DOM」第 625–637 段(
text/18-fm-introduction.txt:625,搜「createElement()」;:633,搜「dynamically created paragraph」;:637,搜「append」)。 ↩ ↩2 -
出处:「Working with the DOM」第 747 段(
text/18-fm-introduction.txt:747,搜「function that waits」)。原文:「An event listener is a function that waits for an event to occur and executes a specified action when the event is triggered. When an event is triggered, JavaScript passes an event object to the event listener.」 ↩ ↩2 -
出处:「Working with the DOM」第 749–753 段(
text/18-fm-introduction.txt:751,搜「addEventListener(eventType」)。原文:「element.addEventListener(eventType, callbackFunction, useCapture);」 ↩ ↩2 ↩3 -
出处:「Working with the DOM」第 643–741 段(
text/18-fm-introduction.txt:653,搜「Mouse events」;:683,搜「Keyboard events」;:697,搜「Form events」;:715,搜「Window events」)。 ↩ -
出处:「Working with the DOM」第 35 段(
text/18-fm-introduction.txt:35,搜「event delegation」)——本章目标列出的机制;其定义与用法见第 13 章正文(主拆解在13-build-a-website.md§6)。 ↩ -
出处:「Working with the DOM」第 807 段(
text/18-fm-introduction.txt:807,搜「bubbling phase」)。原文:「Event propagation occurs in two phases: the bubbling phase (default) and the capturing phase.」 ↩ ↩2 ↩3 -
出处:「Working with the DOM」第 805 段(
text/18-fm-introduction.txt:805,搜「single event listener to a parent」)。原文:「Event delegation helps improve performance by attaching a single event listener to a parent element instead of multiple listeners on child elements.」 ↩ ↩2 ↩3 -
出处:「Working with the DOM」第 809 段(
text/18-fm-introduction.txt:809,搜「stops the default action」)。 ↩ ↩2 -
出处:「Working with the DOM」第 815 段(
text/18-fm-introduction.txt:815,搜「readyState」)。原文:「The document.readyState property helps determine whether the document is fully loaded.」 ↩ -
出处:「Working with the DOM」第 29、35 段(
text/18-fm-introduction.txt:29,搜「setAttribute」;:35,搜「event delegation」)。两者列在本章目标中,setAttribute 与 data-* 正文未展开,照实标为缺口。 ↩