资讯动态

原生JS DOM操作全解析:querySelectorAll与getElementsByClassName等API的差异与实战

发布时间:2026/10/20 19:13:07 来源:尧图企业网站定制
很多前端开发干了几年写原生 JS 的时候拿元素基本就靠这几个 API 打天下getElementById、getElementsByClassName、querySelector、querySelectorAll然后改内容又离不开innerText、innerHTML。老实说刚入行那会儿我根本分不清querySelectorAll返回的 NodeList 和getElementsByClassName返回的 HTMLCollection 到底有啥区别也不知道啥时候该用innerText啥时候该用innerHTML反正能跑就行。直到后来因为一个动态集合的坑线上出了 bug才老老实实把这几个东西的底层行为和适用场景全捋了一遍。这篇文章不整虚的就按老前端交流的方式把这几个 API 的原理、差异、性能表现和实际项目里容易踩的坑过一遍。最后还会带一个完整的原生 JS 表格筛选 Demo从选型到实现一步步拆开讲。不管你是刚入门还是干了两三年想补基础这些内容应该都能用得上。1. 先搞明白为什么 JS 里有这么多“找元素”的方法早期 Web 开发里JS 操作 DOM 最常见的方式就是通过getElementById和getElementsByClassName这两兄弟。后来随着页面结构越来越复杂特别是前端开始大量使用 CSS 选择器作为统一的查风格模式浏览器才逐渐普及了querySelector和querySelectorAll。现在项目里你可能四者都在用但很多人其实没想过一个问题既然有了querySelectorAll能搞定一切为什么老 API 还没被淘汰1.1 getElementById 和 getElementsByClassName 的“出身”getElementById是最早一批进入 DOM 标准的方法它的特点非常鲜明根据元素的 ID 精确查找返回的是一个元素对象如果找不到就返回null。因为 ID 在文档里必须唯一所以这个方法的行为足够简单直接。getElementsByClassName出现在 HTML5 时代以前作用是按照 class 类名查找元素返回一个类似数组的对象。注意它返回的不是单个元素而是一组元素。早期很多老代码喜欢用document.getElementsByClassName(item)[0]这种写法就是因为它的返回值是按索引访问的集合。这两者在当时能满足大部分需求但有一个天然短板只能按一个维度查。你想通过“某个父级容器里的、同时具备两个 class 的元素”就只能先拿到集合再循环过滤代码写起来确实啰嗦。1.2 querySelector 和 querySelectorAll 的“后来居上”querySelector和querySelectorAll的出现让“通过选择器找元素”这件事统一了。它们接受的参数是合法的 CSS 选择器字符串什么意思呢你在样式表里怎么写就能在 JS 里怎么查。比如#header .nav li.active这种复合结构以前用getElementById加getElementsByClassName得写好几行现在document.querySelectorAll(#header .nav li.active)一行就出来了。这才是现代浏览器里更推荐的方式之一因为它能表达非常复杂的查询条件。但这里有个很多人第一次接触时容易懵的点querySelector只返回第一个匹配到的元素querySelectorAll则返回所有匹配元素的集合。换句话说它们的作用范围不一样。还有个容易忽略的地方是querySelector的参数虽然叫 CSS 选择器但如果选择器字符串写错了它会直接抛SyntaxError这一点和getElementById的行为不同老 API 不支持复杂选择器所以也不存在“选择器写错”的问题。2. 两个家族的返回值差别决定了你会不会踩坑我见过太多人在这一步翻车。以为querySelectorAll返回的是数组直接.push()就炸了。也有人在循环里用getElementsByClassName动态添加同类元素结果死循环。这些坑的根源就是返回值到底是NodeList还是HTMLCollection以及它是静态的还是动态的。2.1 getElementsByClassName 返回的是 HTMLCollectionHTMLCollection是一种类数组对象它包含元素集合并且是**实时live**的。这个“实时”太关键了你在脚本里先获取了一个 class 为item的集合然后往页面里新增一个同样 class 的节点这个集合的长度会自动加一。听起来很方便但实际写代码时很容易被坑。最常见的就是“边遍历边删除”const items document.getElementsByClassName(item); for (let i 0; i items.length; i) { items[i].remove(); }循环里删掉第一个元素后items会自动更新原来的第二个元素变成了第一个但i已经变成 1结果就是跳过了原来排在第二位的新元素。本质上你遍历集合的同时集合本身在变化逻辑就错乱了。另外HTMLCollection可以用nameItem或namedItem这类方法访问但它的原型链上的是HTMLCollection.prototype没有数组的map、filter、forEach。你需要先Array.from(xxx)或[...xxx]转换才能用数组方法。2.2 querySelectorAll 返回的是 NodeListquerySelectorAll返回的是NodeList。很多人以为NodeList也是实时的这是误解。规范里明确说querySelectorAll返回的NodeList是静态快照。你在获取之后再去修改 DOM这个NodeList不会自动增加或减少成员。这一点在实际工程里是好事。比如你获取某组按钮后即使后续其他代码在页面里新增了同样的按钮按钮旧的NodeList依然只包含原来的元素不会出现“遍历一半忽然多了个节点”的情况。代码的可预测性大大增强。NodeList通常还带有forEach方法可以直接遍历这是HTMLCollection不具备的。不过它依然不是真正的数组没有map、filter这样的方法需要转换或手动用Array.prototype.slice.call处理。在现代浏览器里还可以直接用Array.from。2.3 静态与动态最隐蔽的性能陷阱静态和动态的另一层影响是性能。动态集合每次访问其成员、长度时浏览器都要重新查询一遍以反映当前文档状态。比如document.getElementsByClassName(foo).length不管你读多少次它都会去重新做一次文档扫描。这在循环中特别致命你可能不知不觉就触发了大量重复查询。我在项目里实际遇到过一个问题有个函数要在滚动事件里反复判断一个区域里有几个.active的项。最初用的是getElementsByClassName(active).length低端手机上滚动明显卡顿。后来把代码改成document.querySelectorAll(.active).length虽然本质还是查询但每次取值不再是动态集合性能确实好了一些。再加上缓存了结果卡顿基本消失。不过也要说明现代浏览器对动态集合的查询优化已经做了很多一般页面差距不会夸张但在大列表高频操作场景下静态集合的可预期性还是更舒服。3. innerText 和 innerHTML看着像干的事完全不同取到元素之后最常用的操作就是改内容。innerText和innerHTML这哥俩长得像行为差异却非常大。用错场景轻则多出几段div字符串重则直接弹出 XSS 漏洞。3.1 innerText处理的是“看得见的文本”innerText给元素设置文本内容时会把内容中的 HTML 标签当作文本原样输出。也就是说你写elem.innerText bhello/b;页面上显示的是字面量bhello/b而不是加粗的 hello。这个行为在大多数场景下都是安全的你把它当作文本处理不用担心里面包含的标签被浏览器解析。innerText另一个特征是它关心 CSS 渲染后的结果。它不会返回那些被样式隐藏比如display:none的文本内容只返回“用户能看到的文字”。这个行为在某些场景下反而是一种负担比如你想读取隐藏模板里的文本时它会返回空字符串。3.2 innerHTML解析 HTML 字符串小心 XSSinnerHTML就完全不同了。它在赋值时会解析 HTML 字符串并构建对应的 DOM 子树等于你传进去的是一段活的页面结构。如果想要插入一段带格式的内容innerHTML确实非常方便。方便的另一面就是危险。如果赋值的内容里有用户输入比如评论、昵称、搜索词又没做过滤用户就能在页面里注入任意 HTML这个向量轻则改页面内容重则盗取 Cookie、伪造登录框就是典型的 XSS跨站脚本攻击。前端这个领域凡是把不可信数据直接塞进innerHTML的代码都埋了雷。即使不是恶意攻击还有一层性能问题。innerHTML赋值时浏览器需要销毁原有子节点并重新解析一段 HTML 字符串这个过程比单纯设置文本要昂贵得多。频繁在列表项里用innerHTML拼接大段字符串页面卡顿是会肉眼可见的。关于这个我后面在性能对比里会特意贴一个测试思路。3.3 还有 textContent很多人搞混聊到这里不得不提textContent。很多入门文章是把innerText和textContent混着讲的但它俩行为不完全相同textContent不管元素是否被 CSS 隐藏都会返回全部文本内容。它也不处理换行排版导致的样式差异只按照 DOM 结构直接拼接所有后代文本节点。在设置场景下textContent和innerText的效果差不多都会把内容当作文本但textContent的浏览器兼容性更好性能也通常更优些因为不需要做布局相关计算。从我个人的习惯看单纯要插入一段纯文本、不想被 HTML 解析时优先用textContent只有明确需要读“用户看到的那部分渲染后文本”时才考虑innerText只有要生成一段结构代码时才碰innerHTML。这么分思路会比较清楚。4. 实操环节5 分钟写一个原生 JS 的表格搜索过滤光讲原理不够来做个能落地的场景。我模拟一个常见需求页面上有一个商品信息表格顶部一个搜索框输入关键词后过滤表格行只显示匹配的商品。这种功能用原生 JS 做是最合适的不用引任何框架。关键点正好覆盖了上面讲的选择器 API 和文本处理 API很适合拿来综合练手。4.1 需求与初步设计表格结构大概长这样input typetext idsearch placeholder输入商品名搜索 table idgoodsTable thead tr th商品/th th分类/th th价格/th /tr /thead tbody tr td机械键盘/td td外设/td td399/td /tr tr td电竞鼠标/td td外设/td td199/td /tr tr td27寸显示器/td td显示设备/td td1499/td /tr /tbody /table需求是输入“鼠标”就保留包含“鼠标”的行其余隐藏。输入为空时显示全部。先想一下该怎么取元素。搜索框肯定用getElementByIdID 唯一性能最好没必要用别的。表格要过滤的行都在tbody里按 class 标记一下比较方便tbody idgoodsBody tr classgoods-row ... /tr /tbody这里就面临第一个选型取所有.goods-row用getElementsByClassName还是querySelectorAll按我前面说的这个场景用querySelectorAll更合适原因有三我们需要的是静态快照。过滤过程中只是做显隐切换没有增删表格行静态集合不会出现动态集合那种在循环里长度跳变的问题。NodeList有forEach写起来更干净。后续如果选择器要升级成.goods-row[data-visibletrue]这样的复杂条件querySelectorAll不用改 API。4.2 完整实现代码与逐行解析const searchInput document.getElementById(search); const goodsRows document.querySelectorAll(.goods-row); function filterGoods(keyword) { const lowerKeyword keyword.trim().toLowerCase(); goodsRows.forEach((row) { const cells row.querySelectorAll(td); let rowText ; // 用 textContent 拼接当前行所有单元格的文本而不是 innerText cells.forEach((cell) { rowText cell.textContent ; }); const isMatch rowText.toLowerCase().includes(lowerKeyword); if (lowerKeyword ) { row.style.display ; return; } row.style.display isMatch ? : none; }); } searchInput.addEventListener(input, (e) { filterGoods(e.target.value); }); // 初始化一次即使什么都没输入也能确保显示完整表格 filterGoods();逐行解释几个关键决策第一为什么用textContent而不是innerText因为单元格里的文本没有隐藏内容textContent不需要触发渲染层级的布局计算性能更好行为也更可预期。第二为什么关键词要toLowerCase()用户在搜索时可能会输入大写或小写商品名称里也可能混着英文统一转小写做比较能避免匹配不到。第三为什么lowerKeyword 要提前恢复所有行显示这是搜索框被清空时的兜底逻辑避免表格一直停留在上次过滤后的状态。第四为什么用row.style.display而不是操作 class显隐切换是临时状态直接用内联样式最简单不会影响原有 class 的复用。4.3 这套写法踩过的坑与优化空间这个 Demo 看上去简单但至少有三个地方属于“只有被坑过才会注意到”的细节。第一个坑是关于nodeList.forEach的兼容性。老版本浏览器不支持如果你要兼容到旧环境建议先转成数组Array.from(goodsRows).forEach(...)或者干脆用 for 循环。做 to B 项目的时候兼容标准还真不能只看个大概。第二个坑是搜索时对英文部分的大小写处理。如果数据里存的是USB 键盘用户搜的是usb不统一大小写就会搜不到。我上面代码已经处理了但真实场景里你还要考虑全半角空格比如用户输入“键盘 ”尾部带个空格trim()就很有必要。第三个坑是性能。这个小表格没问题但如果表格有几千行每次输入都遍历所有行肯定是有成本的。可以加一个“防抖”debounce让输入停顿 200 毫秒后再过滤let timer null; searchInput.addEventListener(input, (e) { clearTimeout(timer); timer setTimeout(() { filterGoods(e.target.value); }, 200); });这种方式在输入过程中每按一个键都不会立刻触发过滤而是等人停下再算实际体验会更顺畅尤其适合数据量大的列表。5. 实战中的典型坑与排查技巧从原理到 Demo最后再把我在真实项目里见过、亲身踩过的坑集中盘一遍。这些内容很多是文档里不会写的但对排查线上问题非常有帮助。5.1 为什么 getElementsByClassName 取到的元素总是“少一个”某次我调试一个动态列表用getElementsByClassName拿到了所有列表项然后在一个按钮点击事件里动态插入新列表项同时想把“总数”更新到一个标签上。代码如下function updateCount() { const items document.getElementsByClassName(list-item); document.getElementById(count).textContent items.length; }这个函数每次点击都调用一遍看起来是新获取集合实际没问题。真正让人头疼的是另一种写法把集合存在全局变量里后续直接用const items document.getElementsByClassName(list-item); function updateCount() { document.getElementById(count).textContent items.length; }因为动态集合是“活的”动态插入新节点后items.length自动更新这一版本反而没问题。麻烦的是反过来——你期望它更新它没更新你期望它不变它却变了。最典型的“少一个” bug 出现在用getElementsByClassName删除节点时。前面讲过循环里remove()会导致集合实时变化从而跳过一些元素。这时候排查问题的思路要转向“集合是否一直保持实时同步”。如果你要用动态集合写删除逻辑一定要倒序遍历const items document.getElementsByClassName(list-item); for (let i items.length - 1; i 0; i--) { items[i].remove(); }倒序删除即使集合动态变化也不影响前面尚未访问的索引这个技巧在真实项目里非常实用。5.2 querySelectorAll 的 forEach 陷阱querySelectorAll返回的NodeList虽然是静态的且自带forEach但如果你对集合中的元素进行替换、删除操作事情就变得微妙起来。需要特别注意的是NodeList的forEach不会像数组那样给你中途 break 或跳出循环的能力。想提前终止遍历你只能用for...of配合 break或者转成数组后用some、every。另外很多人误以为querySelectorAll返回的元素“永远是查到的这些”但实际上当你把集合中的某个节点从文档里移除这个节点对象依然存在于NodeList中只是不在文档流里了。你拿它做style.display修改也不会报错只是页面效果跟着变。检查这类问题时先看“元素是否还挂在文档树上”再去看代码逻辑方向就对了。还有个特别常见的误用querySelectorAll的结果不能直接.map()。虽然NodeList有forEach但没有map所以你要转换const texts Array.from(document.querySelectorAll(.item)).map(el el.textContent);现在用Array.from或者展开运算符[...document.querySelectorAll(.item)]都可以。别在面试或代码 review 时写出querySelectorAll().map的错误。5.3 innerHTML 的性能陷阱与替代方案innerHTML的高频使用场景是动态渲染一个列表或表格。很多人习惯这样写const container document.getElementById(list); let html ; for (const item of data) { html div classcardh3${item.title}/h3p${item.desc}/p/div; } container.innerHTML html;数据少的时候没感觉一旦列表长度到几百上千每次字符串拼接再让浏览器重新解析 HTML开销是很明显的。更严重的是每个item里的字段如果来自接口、用户输入直接拼进 HTML 字符串就存在 XSS 风险。一个常见的优化是用“创建节点”的方式来替代innerHTML拼接比如document.createElementappendChild。虽然代码量大一点但性能和安全性都更好const container document.getElementById(list); container.replaceChildren(); for (const item of data) { const card document.createElement(div); card.className card; const title document.createElement(h3); title.textContent item.title; card.appendChild(title); const desc document.createElement(p); desc.textContent item.desc; card.appendChild(desc); container.appendChild(card); }注意我用的是textContent而不是innerHTML去填文本这样数据里的script或img onerror都不会被当成 HTML 解析安全性和可维护性都上来了。真实项目中如果只是渲染纯文本列表这个方案的性价比非常高。不过也要承认innerHTML在“完整替换一大块静态模板”的场景里依然很方便。比如你通过 AJAX 拿回一段服务端渲染好的 HTML直接container.innerHTML serverHtml是最直接的方式。关键是要分清数据来源不要盲用在用户可控的内容上。5.4 选型建议速查为了让文章最后有个可以抄作业的总结部分我把这几个 API 的使用建议整理成了一个快速参考表。日常开发拿不准的时候直接对号入座。场景推荐 API原因查唯一 ID 元素getElementById精确高效返回单个元素无需遍历按单个 class 取一组元素且需要实时动态更新getElementsByClassName动态行为在某些场景如自动同步计数有优势按复杂 CSS 选择器取一个元素querySelector支持优雅的选择器语法按复杂 CSS 选择器取一组元素且希望静态快照querySelectorAll静态集合更可控支持forEach只想插入纯文本textContent不会被 HTML 解析性能好安全性高想插入 HTML 结构innerHTML直接解析字符串但要注意 XSS想读取用户当前可视范围的文字innerText符合视觉渲染结果但性能略差写在最后的经验分享做了这么长时间前端我的个人体会是DOM API 本身不难难的是搞清楚每个 API 背后的行为语义尤其是“实时集合 vs 静态集合”“HTML 解析 vs 纯文本解析”这两条线。很多线上问题表面看起来像是逻辑写错了实际推下去都会归宿到这几个底层差异上。querySelectorAll和getElementsByClassName的选择不是简单的“谁替代谁”而是看你要的是静态快照还是实时集合。同理innerText和innerHTML、textContent三者之间也不存在“哪个绝对最好”你只要记住一句话能不用innerHTML就别用尤其不要直接往里面拼用户输入能不在循环里反复读取动态集合的length就别读。最后再分享一个小技巧排查这类 DOM 问题的时候不要只盯着 JS 代码看先在浏览器控制台跑一行console.dir(你的集合对象)看看它的原型链和constructor到底是NodeList还是HTMLCollection差距一眼就出来了。很多时候bug 不是“写错了”而是“用错了返回值的类型”。把这条思路记住前端这块儿的不少坑就都能提前避开了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑