ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DOM获取元素方法详解:接口差异、动态集合与选型指南

DOM获取元素方法详解:接口差异、动态集合与选型指南 我做了几年前端带过新人、也面试过不少人发现一个很有意思的现象很多同学在框架里写了半年组件Props、状态管理、自定义Hook样样都通但你冷不丁问一句“JavaScript获取DOM元素的方法有哪些”他当场就卡住了。倒也不是不会而是平时都用框架的指令和模板语法原生DOM交互反而生疏了。但DOM获取元素是前端最底层的功夫——不管是写原生JS、做jQuery插件还是面试聊事件委托、虚拟DOM底层都离不开“先拿到那个元素”。这篇文章就把所有常用方法串一遍各自的返回值、动态还是静态、性能差异、适用场景以及我这些年踩过的坑。新手可以当系统索引老手也可以查漏补缺。1. 先搞懂DOM到底是什么我们究竟在“获取”什么1.1 浏览器眼中的HTML是一棵倒过来的树很多教程喜欢一上来就罗列方法但如果不先理解DOM树后面那些返回值类型HTMLCollection、NodeList就很容易混淆。浏览器解析HTML时会把标签、文本、属性、注释全部转换成一个个节点对象组成一棵树。树根是document往下是html、head、body再往下是各种元素节点。这棵树里节点分好几类元素节点比如div、p、文本节点标签中间的文字、属性节点、注释节点等。而我们日常说的“获取DOM元素”绝大多数情况下拿的是元素节点。理解这一点很重要因为Node接口是所有节点的父接口Element接口才是元素的抽象很多方法返回的类型不一样根源就在这里。1.2 两套接口体系一个历史遗留为什么获取一个元素会有十几种方法归根结底是Web历史演进的结果。早期浏览器厂商各自实现了全套getElementByXXX后来标准化组织把这些收编进DOM规范再后来随着CSS选择器越来越强大W3C又推出了querySelector系列提供了一套“用CSS语法来找元素”的通用方案。所以你会看到两套体系并存一套是getElement系列按标签名、类名、ID、name属性去匹配历史悠久、兼容性极好另一套是querySelector系列按CSS选择器匹配灵活、现代、能组合条件。两套都活着没有谁淘汰谁只是适用场景不同。提示别指望“背一个万能方法走天下”。真正写多了会发现方法之间不是简单的替代关系而是互补关系。选择哪一个取决于你要拿几个元素、拿到的集合是动态还是静态、以及代码要兼容到什么年代。2. 经典姿势逐个拆getElementById与getElementsBy系列2.1 getElementByIdID查找为什么是单数document.getElementById(app) 是前端最经典的一行代码。它按id属性精确匹配返回值是一个元素Element对象如果没有找到就返回null。为什么是单数理论上HTML标准要求id在页面内唯一所以一个就够了。这里有几个容易被忽略的细节。第一这个方法严格区分大小写getElementById(App)和getElementById(app)完全是两码事。第二如果HTML里id是动态拼接产生的注意字符串拼接时别传成数字类型先转成字符串再传。第三元素还没有渲染到DOM树之前调它会返回null这个问题我放到第6章重点说。总结起来getElementById是单元素查找里性能最优、语义最明确的一个日常用得最频繁。2.2 getElementsByTagName与getElementsByClassName注意复数名字和动态集合这两个都是复数方法返回的是一个HTMLCollection——一个类数组的实时集合。什么意思“实时”意味着这个集合是活的DOM里新增或删除了匹配的标签或类集合内容会跟着自动变化拿到的不是快照而是引用。getElementsByTagName(div)会返回页面所有divgetElementsByClassName(box)会返回所有带box类的元素。有个容易踩的点getElementsByClassName支持传多个类名比如getElementsByClassName(box active)只有同时具备这两个类的元素才会被选中且类名顺序无所谓。这和CSS选择器的.box.active是等价语义但很多初学者第一次看到这个写法会误以为它只认一个类。2.3 getElementsByName表单场景里的特殊存在这个方法的匹配依据是name属性主要用在表单里。比如一组单选框都有namegenderdocument.getElementsByName(gender)就能一次全拿到。它的返回值是NodeList不是HTMLCollection这也是个冷知识。而且应用范围比getElement系列窄很多一般只在处理表单时才会主动想起它平时别滥用。有一点要注意getElementsByName不仅仅能查到表单控件任何带name属性的元素都会被命中。所以如果你页面里恰好在普通div上写了name属性它也会出现在结果里。这个行为符合规范但第一次遇到的人很容易发懵。2.4 HTMLCollection和NodeList到底差在哪很多初学的小伙伴在这里彻底懵了——同样是“复数获取”为什么返回的类型不一样我用一个表来说明返回类型包含内容是否动态常见方法是否支持forEachHTMLCollection只有元素节点是getElementsByTagName / getElementsByClassName现代浏览器基本不支持NodeList可以是任意节点可能是静态也可能是动态querySelectorAll静态/ getElementsByName动态支持现代浏览器HTMLCollection只有元素节点所以它身上的属性和方法更“纯”NodeList可以包含文本节点、注释节点等。动态性方面getElementsBy系列始终是动态的querySelectorAll返回的是静态NodeList。至于forEachHTMLCollection在旧浏览器里不能直接用需要转成数组这个问题在第6章细说。提示判断一个方法返回的到底是不是“活的”最简单的实验方法是先获取集合再往页面插入一个匹配元素回头用console.log看长度有没有自动变化。实验一遍比背概念记得牢。3. 现代选择器方案querySelector和querySelectorAll到底强在哪3.1 用CSS选择器“降维打击”document.querySelector(css选择器) 和 document.querySelectorAll(css选择器) 是现代浏览器普及度最高的接口当年第一次用的时候让我感觉到“写JS和写CSS终于统一了”。CSS能写的选择器这两个方法基本都能用标签选择器、类选择器、ID选择器、属性选择器、伪类、后代选择器、兄弟选择器甚至可以组合。比如“页面里data-typecard的div中的第一个p元素”用getElement系列你得先拿div集合再循环遍历找p写一堆代码用querySelector一行搞定document.querySelector(div[data-typecard] p)。这种表达能力就是它最大的价值选择器的复杂匹配逻辑被浏览器原生处理掉了。3.2 querySelectorAll返回的是静态NodeList很多教程只告诉你querySelectorAll返回NodeList但没强调最关键的一点它是静态快照。集合在获取那一刻固定下来之后DOM再变集合长度也不会变。这点和getElementsBy系列的动态行为恰恰相反。动态和静态各有什么优劣动态集合省内存、实时反映页面状态适合做“筛选器”一类的场景但很容易在循环里因为长度变化出bug。静态集合拿到的是当时的快照适合先筛选一次、后面反复使用的场景行为也更符合直觉。不能说哪一个更好关键是你得知道当前拿的是哪一种写逻辑时才能判断它会不会“自己变”。3.3 三个使用细节伪类、错误与兼容性用querySelector有几个注意点。第一选择器虽然支持伪类比如:first-child、:nth-child但像::before、::after这种只能用于渲染的伪元素是选不到的因为它们不是真实节点DOM查询自然查不到。第二选择器写错会直接抛异常SyntaxError比如括号不匹配这一点和getElement系列静默返回null不同——这是好事写在try/catch里能更早暴露问题。第三如果要兼容IE8及以下就得老老实实回到getElementById这在老项目里依然现实。3.4 现代项目里我实际怎么用它在组件化开发里我已经很少把querySelector用在全局了更多是拿到一个元素后在其内部继续查找比如el.querySelector(.item)——这样只搜索el的后代效率更高、也是合理的封装边界。至于querySelectorAll在需要“过滤一组元素并批量绑定事件”的场景中非常顺手因为它支持forEach可以直接展开处理。比如有人喜欢在控制台里写一行脚本document.querySelector(video)拿到视频元素改旋转角度这就是典型的“元素定位”需求。querySelector最擅长的就是这种需要精确锁定的场景但注意别为了用它而去全局搜局部查找时承接的元素必须是确定存在的。注意document.querySelector(#id)在语法上完全正确但既然页面有更专业的getElementById就没必要绕一圈。工具要选对的而不是选多的。4. 不用写方法的快捷通道body、forms、children与关系导航4.1 document.body这类固定入口有些元素根本不需要“获取”文档对象直接把入口摔在你脸上。document.body就是页面body元素document.documentElement是html元素document.head是head元素。写脚本判断页面是否滚动到顶部时你拿着document.documentElement.scrollTop或者document.body.scrollTop直接读就完事不需要任何选择器。还有document.title可以直接读写页面标题document.URL拿当前地址。这些虽然不完全算“获取元素方法”但都属于“不查询也能访问”的文档级快捷方式实际开发里非常常用值得放在一起记。4.2 文档级集合属性forms、images、links、scriptsDOM规范还给document挂了一组快捷集合属性document.forms是所有表单、document.images是所有图片、document.links是所有带href的a标签、document.scripts是所有script标签。这些集合返回的都是HTMLCollection。它们的价值在于语义化你想遍历页面上所有表单去做校验时直接document.forms搭配遍历比querySelectorAll(form)更直白代码读起来也更接近业务语义。我做过一个需求页面加载完成后自动给所有未填写的input加上红色边框。当时就是document.forms先拿到所有表单再遍历表单里的elements注意表单本身也有elements集合属性整个过程完全没写一个选择器。这种场景下语义化快捷通道比通用选择器好用得多。4.3 通过关系导航拿元素children、parentNode、nextElementSibling还有一种常见的“获取”不是通过选择器而是通过已掌握的元素往周围走elem.children拿到所有子元素elem.parentNode拿父节点注意是Node类型偶尔需要再判断nodeTypeelem.nextElementSibling拿下一个兄弟元素节点elem.previousElementSibling拿上一个。这几个在写折叠面板、树形组件时几乎是标配。这里有个经典细节children只包含元素节点childNodes则包含文本、注释节点。很多人写遍历时用了childNodes结果被空文本节点折磨到怀疑人生——HTML里换行产生的空白字符也是文本节点childNodes会把它们都列出来。我的建议是默认操作“元素”时优先用children除非你真的要处理文本节点。4.4 老接口的新用法elem.querySelector和closest最后补两个容易被忽略的现代API。elem.querySelector(selector)和elem.querySelectorAll(selector)会在“以当前元素为根的子树”里查找这就实现了局部作用域elem.closest(selector)则是反着走从当前元素开始往上找最近一个匹配选择器的祖先元素找不到返回null。这两者对事件委托场景非常友好我以前写“点击li找最近的table行”就靠closest一行解决嵌套层级带来的麻烦。closest还有一个值得注意的行为它从当前元素自身开始判断而不是从父节点开始。也就是说elem.closest(div)如果elem本身就是div会直接返回elem。这个细节很多文档没强调实际写事件委托时恰恰会因此写出更简洁的代码。5. 实操选型同一个页面我按什么标准挑获取方式5.1 从需求反推单元素、多元素、动态集合还是静态快照我先给个最简单的判断框架要拿一个元素优先getElementById要拿多个元素并且希望实时反映页面变化用getElementsBy系列要拿多个元素并且只关心当前状态用querySelectorAll要通过复杂的组合条件定位用querySelector要在已知元素周围导航用children、parentNode、closest。这个框架不是教条但能覆盖日常90%的场景。还有一条隐藏判断标准你要的是“一批元素”还是“一个元素”。很多人写批量操作时习惯用querySelectorAll但拿到的是NodeList虽然支持forEach但它不是真正的数组想用map、filter还得Array.from转一下。如果你明确只要一个querySelector就够返回的就是Element不需要再从集合里取[0]。5.2 一个真实页面场景的拆解举个例子一个后台管理页面里有动态渲染的表格每行一个“删除”按钮点击后先弹出确认框确认后删除行。怎么做我会先给表格容器加一个id然后事件委托table.addEventListener(click, (e) { const btn e.target.closest(button.delete-btn); if (!btn) return; ... })。这里我根本没去“全局搜索”按钮而是靠closest在事件流里定位目标。如果每行创建时就给按钮绑定click再配合querySelectorAll(.delete-btn)来批量为已有行绑定也是常见做法。两种思路对应不同性能模型前者只绑定一次事件事件冒泡到table统一处理适合行数很多的场景后者绑N次但逻辑更直观适合行数少、结构稳定的场景。面试里聊事件委托时说的“性能优化”底层就是这两种模型的取舍。5.3 性能与兼容性的现实考量用直觉得出“querySelectorAll比getElementsByClassName慢”这种判断是不可靠的我实测下来两者量级差异在现代浏览器里几乎可以忽略真正影响性能的是一个查询的复杂度尤其是选择器写了很长很长的后代组合时浏览器要跑匹配算法。更实际的影响是动态集合的生命周期——如果在循环里反复读集合的length动态集合每次都会重新统计极端情况效率很低所以我会在需要连续使用时先把集合转成数组存起来。兼容性上偏老的项目优先getElement系列现代项目以querySelector为主这算是我的一条经验线。不过这些年我也发现“兼容性”对很多业务来说已经不是首要矛盾了反而代码的可读性和维护成本更重要。你想想同事看到document.querySelector(div[data-typecard] p)和看到三行for循环哪个更能一眼看出意图选择器往往是前者。5.4 我的个人选择习惯写到这里我得说一点个人习惯供参考全局查找用getElementById打头局部查找用elem.querySelector批量拿一组相对孤立的元素用querySelectorAll需要在树里上下级跳转时用关系导航表单校验优先document.forms。这套组合用了很多年少踩了不少坑下面分享几个最常见的坑。6. 高频踩坑实录拿不到元素、forEach报错、动态集合死循环6.1 为什么getElementById总是拿不到元素这个问题十次有八次是脚本执行时机造成的。script标签放在head里此时body还没解析DOM树里根本没有那个id对应的节点自然返回null。解决办法有三个把script放到body末尾使用DOMContentLoaded事件或者把代码包在defer脚本里。还有一个隐蔽原因页面里真的有两个相同id的元素这在动态拼接页面中不罕见虽然标准说id必须唯一但浏览器不报错getElementById只会返回第一个给调试造成困扰。排查方法很简单在控制台里直接输入document.getElementById(你的id)看返回。如果返回null先确认Elements面板里这个元素确实存在再确认脚本执行时DOM是否已经加载。这两个确认完90%的“拿不到”问题都能解决。6.2 为什么HTMLCollection不能直接用forEach写惯了数组操作的同学第一次用document.getElementsByClassName(box).forEach(...) 会直接遇到TypeError。原因我在2.4已经说过了它不是NodeList标准设计本身就不包含forEach方法。解决路径有两条转数组Array.from(collection)后用完整数组方法或者直接用querySelectorAll从源头拿到支持forEach的NodeList。至于为什么标准偏偏不统一只能说是历史遗留习惯就好。这个坑在面试里经常被反转出来考面试官会问“如果HTMLCollection没有forEach你怎么遍历它”答案是for循环、Array.from、for...of都可以。而且好消息是绝大多数现代浏览器的for...of是支持遍历类数组对象的只要它有length属性和索引下标。6.3 动态集合引发的“无限循环”这是我最想提醒的一次事故。假设你要把页面所有div里的空div删除用getElementsByTagName(div)拿集合后写for循环每删一个空节点动态集合的长度就减一如果循环条件用初始的let i 0; i length; i根本步进不到真实边界轻则漏删重则数组越界。正确做法是倒序遍历或者先把集合转成静态数组再遍历。这可以说是动态集合最经典的陷阱面试里聊DOM时也经常被问到。// 动态集合倒序遍历避免索引错位 let divs document.getElementsByTagName(div); for (let i divs.length - 1; i 0; i--) { if (divs[i].textContent.trim() ) { divs[i].parentNode.removeChild(divs[i]); } }getElementsByClassName同理。只要记住“动态集合的长度会自己变”写循环时多留个心眼这个坑基本就能绕过去。这也是为什么很多风格指南建议“遍历DOM集合前先转成数组”。6.4 “缓存”的坑重复查询损耗与集合快照如果在一个函数里反复document.getElementById同一个id性能损失很小但显得代码很业余更关键的是动态集合保存变量后变量引用的是实时集合不是当时的值。我见过有人先let items document.getElementsByClassName(item); 然后做了一堆DOM操作最后以为items还是之前那批元素结果逻辑全乱。明白动态集合语义后这类bug基本能一眼识别。反过来有人以为querySelectorAll的结果会实时更新结果操作完DOM再遍历发现还是旧的这也是对静态快照不理解导致的。两类集合的更新时机恰恰相反用的时候先问自己一句“我拿到的到底会不会变”能避免大量莫名其妙的bug。6.5 别让innerHTML和动态拼接毁掉安全底线最后补一个和获取元素强相关的安全问题。拿到元素后很多人习惯直接element.innerHTML 用户输入这在某些场景下会引入DOM型XSS。比如你用一个输入框的内容拼到innerHTML里用户输入一段包含img onerror的字符串脚本就可能在页面里执行了。正确的做法是优先用textContent或者用createElement appendChild去构造节点。我处理动态列表时一直都坚持“宁可多写几行构造节点也不用innerHTML拼接用户数据”。// 安全写法构造节点而不是拼接字符串 const div document.createElement(div); div.textContent userInput; // 纯文本不会执行任何标签解析 parent.appendChild(div);这还是面试里经常会顺着往下问的点拿到元素之后你怎么改内容。如果你每次都是innerHTML一把梭对面面试官多半会追问一句“如果内容是用户输入呢”。能答出textContent和createElement的区别这一关才算过。6.6 顺手送你一个调试技巧所有获取元素的问题都可以先打开浏览器控制台在Elements面板确认目标节点存在、id/class拼写正确然后在Console里手动输入document.querySelector(#你的选择器)看返回。返回null就检查选择器写法返回了Element就检查JS执行时机返回了集合就检查是不是动态集合的问题。三步定位法比瞎猜快得多。如果你在调试样式定位问题时想快速试一下选择器直接在浏览器的Console里输入$$(选择器)Chrome DevTools的快捷命令等价于querySelectorAll可以秒速看到命中元素列表比反复改代码再刷新快得多。这个技巧我用了很多年分享给团队后大家都说效率明显提升。说实话DOM获取元素的方法不算多但每个方法背后都藏着一段浏览器演进史为什么有动态集合为什么有NodeList为什么推荐用querySelector又劝你别完全放弃老接口。我个人在实际项目里的体会是这些方法不要靠背靠用——写上几十个小交互你对“什么时候返回HTMLCollection什么时候返回NodeList”就有肌肉记忆了。最后再提一句别只盯着获取拿到元素之后的增删改查同样重要但那就是另一篇文章了。希望这些经验能帮你少走一点弯路。
返回列表