编程语言研究书库

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("类名")按 classHTMLCollection(一组,活的)
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 的元素时,返回 null

querySelector 家族是用 CSS 选择器来挑——第 07、08 章学过的选择器全部通用: querySelector(".highlight") 挑第一个带 highlight 类的, querySelectorAll("ul li") 挑所有列表项。一个返回单件、一个返回集合,找不到都各自返回 null / 空集合。

null 这个返回值要提前认识:第 13 章的调试实战,就是从「getElementById 返回了 null、 代码去读 null 的属性」这个报错开始的。写选择代码时,值可能是 null,是 DOM 编程的常识前提。

4. 修改:内容、属性、样式

选中之后,改法分三路。

改内容,三个属性容易混,书里给了分界的办法5:

属性写什么进去标签会被解析吗
.innerHTMLHTML 源码会——<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)未涉及——异步交互的大门开在本书之外。
  • innerText vs textContent 的细节(性能与渲染依赖)书里只给了概念分界。
  • setAttribute/自定义 data-* 属性只在目标清单里露名18,正文未展开——第 13 章也没有用到,照实标为缺口。

10. 可带走的

全章主线例子,一行写完: getElementById 拿到按钮 → addEventListener 挂上 click 回调 → 用户点击 → 事件对象进回调 → alert 弹出、控制台打印 event。

  1. DOM = 浏览器把 HTML 解析成的节点树,是页面和 JavaScript 之间的接口1;
  2. 选中五法:ById(单个)、ByClassName/ByTagName(集合)、querySelector(第一个)/All(全部,CSS 选择器语法);
  3. 找不到会返回 null / 空集合——对 null 取属性是 DOM 第一大报错源;
  4. 改内容:innerHTML 解析标签、textContent 只当纯文本;改样式用 .style.驼峰属性,更好的是改 classList;
  5. 增删三步:createElement(内存里造)→ 装内容 → append 挂树;remove 摘树;
  6. addEventListener(事件名, 回调):回调「事发了才执行」;事件对象带 target/type/坐标11;
  7. 事件委托 = 监听父级 + 靠冒泡 + 看 e.target;动态元素自动被覆盖,还省监听器15;
  8. 传播两阶段:捕获(外→内)与冒泡(内→外),默认冒泡14;
  9. preventDefault 拦默认动作;readyState/DOMContentLoaded 管「树建好没有」的时序。

11. 原文地图

主题原书章原文位置
DOM 的定义与接口身份Working with the DOMtext/18-fm-introduction.txt:39(搜「tree-like representation」)
五种选择方法与返回值Working with the DOMtext/18-fm-introduction.txt:315(搜「getElementById」) · text/18-fm-introduction.txt:341(搜「returns a single element or null」)
getElementById 的 null 语义Working with the DOMtext/18-fm-introduction.txt:365(搜「or null if none is found」)
innerHTML 改内容Working with the DOMtext/18-fm-introduction.txt:367(搜「innerHTML property」)
textContent / innerText 分界Working with the DOMtext/18-fm-introduction.txt:411(搜「textContent」) · text/18-fm-introduction.txt:419(搜「respects hidden elements」)
改样式与 classListWorking with the DOMtext/18-fm-introduction.txt:501(搜「style.color = "blue"」) · text/18-fm-introduction.txt:435(搜「classList」)
createElement 三步流水Working with the DOMtext/18-fm-introduction.txt:625(搜「createElement()」)
事件监听器与事件对象Working with the DOMtext/18-fm-introduction.txt:747(搜「function that waits」)
addEventListener 三参数Working with the DOMtext/18-fm-introduction.txt:751(搜「addEventListener(eventType」)
事件类型表Working with the DOMtext/18-fm-introduction.txt:653(搜「Mouse events」)
冒泡与捕获两阶段Working with the DOMtext/18-fm-introduction.txt:807(搜「bubbling phase」)
事件委托的性能理由Working with the DOMtext/18-fm-introduction.txt:805(搜「single event listener to a parent」)
preventDefaultWorking with the DOMtext/18-fm-introduction.txt:809(搜「stops the default action」)
readyStateWorking with the DOMtext/18-fm-introduction.txt:815(搜「readyState」)
章末小结:四件事总览Working with the DOMtext/18-fm-introduction.txt:785(搜「tree-like representation」)

Footnotes

  1. 出处:「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

  2. 出处:「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).」 ↩

  3. 出处:「Working with the DOM」第 307–353 段(text/18-fm-introduction.txt:315,搜「getElementById」;:325,搜「getElementsByClassName」;:331,搜「getElementsByTagName」;:339,搜「querySelector」;:347,搜「querySelectorAll」)。五方法的返回类型表出自这一段组。 ↩ ↩2

  4. 出处:「Working with the DOM」第 357–365 段(text/18-fm-introduction.txt:363,搜「getElementById("mainheading")」;:365,搜「or null if none is found」)。 ↩

  5. 出处:「Working with the DOM」第 405–423 段(text/18-fm-introduction.txt:405,搜「innerHTML」;:411,搜「textContent」;:417,搜「innerText」)。 ↩ ↩2

  6. 出处:「Working with the DOM」第 367–395 段(text/18-fm-introduction.txt:369,搜「Welcome to JavaScript」)。 ↩

  7. 出处:「Working with the DOM」第 423–491 段(text/18-fm-introduction.txt:449,搜「value」;:473,搜「checked」;:461,搜「disabled」)。 ↩

  8. 出处:「Working with the DOM」第 497–619 段(text/18-fm-introduction.txt:501,搜「style.color = "blue"」)。 ↩

  9. 出处:「Working with the DOM」第 625–637 段(text/18-fm-introduction.txt:625,搜「createElement()」;:633,搜「dynamically created paragraph」;:637,搜「append」)。 ↩ ↩2

  10. 出处:「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

  11. 出处:「Working with the DOM」第 749–753 段(text/18-fm-introduction.txt:751,搜「addEventListener(eventType」)。原文:「element.addEventListener(eventType, callbackFunction, useCapture);」 ↩ ↩2 ↩3

  12. 出处:「Working with the DOM」第 643–741 段(text/18-fm-introduction.txt:653,搜「Mouse events」;:683,搜「Keyboard events」;:697,搜「Form events」;:715,搜「Window events」)。 ↩

  13. 出处:「Working with the DOM」第 35 段(text/18-fm-introduction.txt:35,搜「event delegation」)——本章目标列出的机制;其定义与用法见第 13 章正文(主拆解在 13-build-a-website.md §6)。 ↩

  14. 出处:「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

  15. 出处:「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

  16. 出处:「Working with the DOM」第 809 段(text/18-fm-introduction.txt:809,搜「stops the default action」)。 ↩ ↩2

  17. 出处:「Working with the DOM」第 815 段(text/18-fm-introduction.txt:815,搜「readyState」)。原文:「The document.readyState property helps determine whether the document is fully loaded.」 ↩

  18. 出处:「Working with the DOM」第 29、35 段(text/18-fm-introduction.txt:29,搜「setAttribute」;:35,搜「event delegation」)。两者列在本章目标中,setAttribute 与 data-* 正文未展开,照实标为缺口。 ↩

On this page