
学到这里终于该碰碰真正的页面了从“JavaScript学习手册”系列一路刷到这儿的同学应该对变量、数据类型、运算符、对象、数组、循环、字符串这些基础概念都不陌生了。但说实话光会这些你还没法在网页上做出任何看得见摸得着的东西。这一篇要聊的HTML DOM才是JavaScript真正从“算数工具”变成“页面操控器”的分水岭。简单说HTML DOM就是浏览器把网页文档变成一棵可供JavaScript操作的树。你写的每一个标签、每一段文字、每一个属性在这棵树里都是一个节点。而这篇《HTML DOM——文档元素的操作一》要解决的核心问题只有一个怎么在JS里把页面上的某个元素精准地找出来并且拿到手之后能对它做一些基础操作。这是所有DOM编程的地基后面你要学的增删元素、事件绑定、表单处理全都要站在这一篇的内容上面。所以如果你是跟着系列一路学过来的初学者或者基础不牢想回头补DOM入门的人这篇就是为你准备的。我先把我自己踩过的坑和这一篇的完整路线图讲清楚你再照着走会顺很多。1. 内容整体设计与思路拆解1.1 为什么学了半天基础语法还是没有“网页感”很多自学JavaScript的人都有个共同困惑循环、函数背得滚瓜烂熟但一打开浏览器发现自己还是不会让页面发生任何变化。原因很简单你之前学的是JavaScript这门语言本身它跑在Node环境也好、浏览器控制台也好和网页上的那堆HTML标签没有任何关系。而DOMDocument Object Model就是语言和网页之间的那座桥。浏览器在解析HTML文档时会把它转换成一个树状结构专业叫法是“节点树”。document是这棵树的根往下是html、head、body这些顶级节点再往下逐层展开。每一个HTML标签成为一个元素节点标签里的文字成为文本节点标签上的class、id、src这类东西成为属性节点。JavaScript能访问和操控的就是这个树结构。你通过JS改某个节点的内容浏览器会立刻把修改反映到页面渲染上——这就是动态网页的基本原理。理解这棵树的抽象模型比死记硬背任何API都重要。因为后面所有操作——查找、遍历、新增、删除——本质都是在这棵树上定位节点、改动节点。如果脑子里没有“树”的概念你会在parentNode、children、firstChild这些属性上绕得晕头转向。1.2 为什么这一篇只讲“获取元素”和“基础操作”一个完整的DOM操作流程可以拆成四步找元素、读元素、改元素、增删元素。考虑到系列教程的节奏这一篇一只覆盖前三步里的核心部分——怎么用各种方式找到目标元素以及找到之后怎么读它、改它的内容与样式。至于创建新节点、插入节点、删除节点这些“增删”动作留到下一篇二里展开。这样划分是合理的。获取元素是整个DOM操作的入口如果连元素都拿不准后面全是空中楼阁。而不同的获取方式返回的东西还不一样有的是元素本身有的是一堆元素的集合这里面的差异值得花一整篇的篇幅消化。同时“找到之后能对元素做什么”也需要先把innerHTML、textContent、style这几个最常用的属性吃透它们能覆盖日常开发里很大一部分需求。2. 核心细节解析与实操要点2.1 获取文档元素的六种手段一张表说清楚JavaScript提供了好几套API来获取元素。初学的时候最容易犯的毛病就是每种方法都会一点但不知道什么场景该用哪个。我先把它们全部列出来对比再逐个讲注意点。方法写法返回内容典型场景按id获取document.getElementById(id)单个元素找不到返回null页面里唯一元素最常用按类名获取document.getElementsByClassName(class)元素集合HTMLCollection动态一组同类元素按标签名获取document.getElementsByTagName(div)元素集合HTMLCollection动态获取某种标签的全部元素CSS选择器获取首个document.querySelector(选择器)匹配的第一个元素找不到返回null灵活、精准定位CSS选择器获取全部document.querySelectorAll(选择器)NodeList静态可用forEach需要批量操作、遍历从某个元素内部继续查element.querySelector(选择器)第一个匹配的后代元素缩小查找范围、避免冲突这六种方法最核心的区别有两个一是返回的是单个元素还是集合二是返回的集合是“活的”还是“死的”。getElementsByClassName和getElementsByTagName返回的HTMLCollection是动态的意思是如果页面上的DOM发生了变化这个集合会自动更新很容易造成遍历时的意外。而querySelectorAll返回的NodeList是静态的它在调用那一刻固定下来后续DOM怎么改它都不变。这个差异在实战里特别坑人我后面专门讲。2.2 getElement系列老牌方法性能好但不够灵活getElementById是我在日常开发里用得最多的没有之一。因为一个规范的页面里id本身就是唯一的。它的性能是所有获取方式里最好的因为浏览器对id做了索引查找几乎不消耗时间。要注意的地方是如果页面里根本没有这个id的元素它返回的是null而不是undefined这时候如果直接去操作它的属性浏览器会报错。所以稳妥的写法是先判断一下再往下走。var box document.getElementById(app); if (box) { // 到这里才放心操作box console.log(box.className); }getElementsByClassName和getElementsByTagName名字里都有个“s”因为返回的都是集合。它们返回的是HTMLCollection虽然是伪数组有length、可以用索引访问但你不能直接对它调用forEach。很多新手在这躺枪转头去问为什么报错。解决办法有两个用Array.from()把它转成真数组或者干脆用querySelectorAll。还有个容易忽略的细节getElementsByClassName的参数可以传入多个类名用空格分隔表示“同时拥有这些类”的元素。例如document.getElementsByClassName(card active)匹配的是既包含card又包含active的元素。2.3 querySelector系列CSS选择器一把梭如果你写过CSSquerySelector几乎不需要额外学习成本。它接受一切合法的CSS选择器类、id、属性、伪类、组合器都行。document.querySelector(#main .card:first-child)这类复杂定位用getElement系列可能要写好几行而这里一行搞定。讲一个实际场景。有一回我要给页面里表格的所有偶数行加背景色用querySelectorAll(tr:nth-child(even))一行就能选中然后遍历设置样式。如果用旧API需要先拿所有tr再自己算索引判断奇偶代码量和出错概率都高不少。querySelectorAll返回的NodeList在现在的浏览器里支持forEach但你要养成习惯它是静态集合。假设你先把某个按钮从页面移除再遍历之前获取的NodeList你会发现那个按钮还在集合里。这在做列表筛选、动态渲染的时候特别容易造成认知偏差。我的习惯是只要涉及集合遍历优先用querySelectorAll它更接近数组的使用方式心智负担小。提示querySelector找不到元素时返回nullquerySelectorAll找不到时返回空的NodeList而不是null。这个差异很关键因为空的NodeList你也可以放心调用forEach不用额外判空。3. 实操过程与核心环节实现3.1 第一步拿到元素之后先学会在节点树里“走路”元素获取到手后你往往不能只操作它一个还要顺着它在树里往上爬、往下走、左右邻。这一节把节点遍历相关的属性一次讲透。和“节点”有关的一组属性是parentNode、childNodes、firstChild、lastChild、nextSibling、previousSibling。注意它们的名字里没有“Element”。它们返回的对象范围更广包含了文本节点、注释节点。这就有个经典的坑HTML里标签之间的换行和空格在DOM树里会被解析成文本节点。你用firstChild想拿第一个子元素拿到的却是一个只包含换行符的文本节点。和“元素”有关的一组属性是parentElement、children、firstElementChild、lastElementChild、nextElementSibling、previousElementSibling。它们只关心元素节点把文本节点和注释节点全部跳过。目标用节点属性用元素属性推荐父级parentNodeparentElement所有子级childNodeschildren第一个子级firstChildfirstElementChild最后一个子级lastChildlastElementChild下一个兄弟nextSiblingnextElementSibling上一个兄弟previousSiblingpreviousElementSibling实际开发里我几乎只用带Element的那一行。只有极少数场景需要对文本节点做操作比如提取一段富文本里的纯文字内容才会用到childNodes。var list document.getElementById(list); var firstItem list.firstElementChild; // 第一个li var secondItem firstItem.nextElementSibling; // 第二个li var parent secondItem.parentElement; // 回到ul这样写读代码的人一眼就知道你在操作元素不会踩到空白文本节点的坑。3.2 第二步修改内容textContent和innerHTML怎么选拿到元素之后最基础的操作就是改里面的内容。两个常用属性textContent和innerHTML。textContent负责纯文本。给它赋值它会把你给它的字符串当成纯文本显示哪怕字符串里带b这种标签也会被原样显示成文字而不是变成加粗效果。反过来读的时候它会把元素里所有文本拼在一起返回不包含任何标签。这在提取用户可见文字的时候很好用。innerHTML则是把字符串按HTML语法解析后再渲染。赋值box.innerHTML b加粗/b页面上会真的出现一个加粗的“加粗”两个字。读取的时候它会返回元素内部完整的HTML源码。这里有一条安全红线永远不要用innerHTML往页面里直接塞用户输入的内容。用户如果提交了一段img srcx onerroralert(1)这样的内容你用innerHTML插进去脚本会在其他用户浏览器里执行造成XSS攻击。最安全的做法是用户内容一律用textContent确需使用innerHTML时必须确保内容是你自己控制的静态模板或者已经做过严格的转义处理。注意这两个属性除了安全性差异还有一个性能差异。频繁修改innerHTML会让浏览器重新解析HTML片段开销比textContent大。如果你只是改一段普通文字用textContent就够了完全没必要动用innerHTML。3.3 第三步操作样式从style到className的进阶路线修改元素样式有两种思路直接改内联样式或者切换CSS类名。直接用style属性改内联样式优点是简单直观、优先级最高内联样式本身优先级高于外部样式表缺点是样式逻辑散落在JS里后期不好维护。而且要注意CSS属性名到JS的转换带连字符的属性要改成驼峰式比如background-color要写成style.backgroundColorfont-size要写成style.fontSizez-index要写成style.zIndex。有个例外是float它是JS的保留字所以要用style.cssFloat。var video document.querySelector(video); video.style.rotate -90deg; video.style.width 50%; video.style.borderRadius 8px;网上有个流传很广的搞笑代码javascript:v document.querySelector(video);v.style.rotate -90deg;用来把网页里横着的视频旋转过来。这个例子恰好说明style操作的直观之处——你几乎可以像写CSS一样用JS给任何元素加上内联样式。rotate这个属性在CSS里也是新出的独立属性和transform: rotate()作用类似直接写在style后面即可。另一种方式是操作className。比如一个元素原本是白色背景要变成红色背景你不用在JS里写backgroundColor只需要提前在CSS里定义好.highlight { background-color: #f00; }然后JS里执行element.className highlight。这样样式表现留在了CSS文件里JS只负责切换状态代码分离更干净。再进一步classList提供了更精细的操作add、remove、toggle、contains。这四个方法是最常用的一组。特别是toggle可以一键实现展开收起、明暗切换这类交互不用自己判断当前有没有某个类再去增删。classList还天然解决了className覆盖整个类名的问题——用className赋值你等于把元素原来所有的类都换掉了而classList.add只是在原有基础上追加一个类不会误伤其他样式类。日常开发我推荐优先用classList。3.4 一个完整的示例给所有卡片绑定点击高亮光讲概念不落地等于白学。我写一个综合小案例把上面所有知识点串起来。假设页面上有一组卡片结构如下div classcard idcard-1苹果/div div classcard idcard-2香蕉/div div classcard idcard-3橘子/div需求是点击任意一张卡片这张卡片高亮其他卡片取消高亮。先准备CSS类.highlight { background-color: #ffe066; border: 2px solid #f0a500; }JS逻辑分三步先拿到所有卡片再给每张卡绑定点击事件事件这块后面章节详细讲这里先用一下最后在事件里切换类名。var cards document.querySelectorAll(.card); cards.forEach(function(card) { card.addEventListener(click, function() { // 先把所有卡片的highlight类移除 cards.forEach(function(item) { item.classList.remove(highlight); }); // 再给当前点击的卡片加上highlight card.classList.add(highlight); }); });这段代码里的querySelectorAll配合forEach放到旧API时代要写更多行。而且你可以看到整个过程完全不需要操作style属性只靠classList切换类名就完成了视觉变化。这就是前面强调的“样式交给CSS、JS只控制状态”的实践。等你学到事件那一篇再回头看会发现这个例子其实就是最基础的“选中状态”交互模型。4. 常见问题与排查技巧实录4.1 为什么脚本放在head里死活获取不到元素新手最常见的报错是document.getElementById(box)返回null代码逻辑看起来完全没问题。原因九成是脚本在body元素渲染完之前就执行了。浏览器解析HTML是自上而下的如果JS写在head里执行的那一刻body里的box还不存在自然拿不到。解决办法有三种把script标签放到body末尾确保DOM解析完再执行脚本。这是最简单也最推荐的方式。用window.onload事件等页面全部加载完再运行window.onload function() { var box document.getElementById(box); // 到这里就一定有值 };如果你的项目用了ES Module或者构建工具可以把script标签加上defer属性它会让脚本在DOM解析完成后执行、同时保持执行顺序。defer比window.onload更优雅因为它不会阻塞后续资源的加载。这个问题的本质是执行时机理解了DOM树是逐步构建的你就会明白为什么有时候拿不到、有时候能拿到。4.2 querySelectorAll返回的不是数组别一上来就mapquerySelectorAll返回的是NodeList它虽然有forEach但没有map、filter、reduce这些数组方法。很多从框架转过来的人习惯直接document.querySelectorAll(...).map(...)然后发现控制台报错map is not a function。解决方式通常是用Array.from()转成真数组var items Array.from(document.querySelectorAll(.card)); items.map(function(item) { return item.textContent.trim(); });如果你觉得这个写法麻烦还可以用展开运算符var items [...document.querySelectorAll(.card)]。效果一样。不过我一般只有在确实需要数组方法时才转换机器只要遍历一遍直接用forEach更省事。4.3 classList.toggle的第二个参数解决条件切换的痛toggle方法最常见的用法是element.classList.toggle(active)它会在“有就删、没有就加”之间自动切换。但有的时候需求不是“无脑切换”而是“根据某个条件来决定是否加上”。很多人的第一反应是if (isChecked) { element.classList.add(active); } else { element.classList.remove(active); }其实toggle支持第二个参数这个参数是一个强制标志如果第二个参数为true就添加类为false就移除类。上面三行可以压缩成一行element.classList.toggle(active, isChecked);这个特性在做表单联动、条件样式时非常好用代码干净不少。我第一次知道的时候后悔了好一阵因为之前写了很多无谓的if else。4.4 快速问题排查速查表症状可能原因解决方案获取元素返回null脚本执行时DOM还没渲染完把脚本放body末尾、用defer或DOMContentLoadedHTMLCollection不能forEach它是类数组不是真数组用Array.from()转换或改用querySelectorAllfirstChild拿到的是空白文本忽略了文本节点改用firstElementChildstyle.backgroundColor报错或无效CSS属性名没有转成驼峰检查连字符写法如zIndex、marginTopclassList操作后原有样式被清掉误用了className赋值覆盖改用classList.add动态改变innerHTML后绑定的事件失效旧元素被替换事件绑定也跟着没了改用事件委托后续章节有详解这几条是我自己在开发里反复踩过的坑尤其是HTMLCollection和NodeList的区别几乎每个学DOM的人都要在这个点上栽一次。我到现在写代码时只要涉及集合脑子里都会先过一遍这个集合是动态的还是静态的要不要转数组操作之后会不会有意外更新想清楚这三个问题能少调半天bug。5. 从这一篇到下一篇你还需要做什么准备这一篇的内容全部围绕“获取元素”和“基础读写操作”展开但DOM的世界远不止这些。你接下来要面对的下一座大山是动态创建和删除元素——document.createElement、appendChild、insertBefore、removeChild这一套。它们是实现列表新增、消息通知、动态表格这类功能的基石。而再往后还有事件处理监听、冒泡、委托、表单操作、节点克隆与替换等等。我的学习建议是这一篇结束后不要急着看下一篇先自己找几个普通网页打开浏览器控制台F12用今天学的方法去获取页面上的各种元素试着改一改它们的文字、切换一下类名、旋转一个图片。控制台是练习DOM操作成本最低的场地你改坏了刷新一下就恢复原样没有任何心理负担。等你把获取元素的手段练到“闭着眼睛都能写出来”的程度下一篇文章里的增删元素操作你会学得非常顺。因为那时候你对“元素在DOM树里是一个节点”这个抽象已经建立了真实的体感后面的一切都只是在这个认识上继续添砖加瓦而已。