做前端这几年,最绕不开的就是 DOM 操作,而 DOM 查询又是所有操作的第一步。你说一个页面几十个节点,想改样式、绑事件、拿数据,不先把元素找出来,后面全是空谈。所以我把自己的项目里最长写的“javascript之Dom查询操作”整理成系列,这是第一篇,重点讲清楚了 querySelector 系、getElement 系这些查询 API 到底该怎么用、什么时候选哪个、有哪些坑是文档里不会写的。
这篇文章适合刚入门前端、或者已经写了一阵子但靠查资料应付的人,也适合回头想系统梳理 DOM 查询知识的朋友。我会从底层原理讲到实际场景,再给你几段可以直接抄的代码和一套排查问题的思路,保证你看完能知道自己每次“选中元素”时,浏览器背后到底发生了什么事。
1. DOM查询的整体思路与方案选型
1.1 为什么“查询”是DOM操作的核心
浏览器把 HTML 解析成一棵树,也就是 DOM 树,每个标签、文本、注释都是树上的一个节点。前端要动态改页面,本质上就是对这棵树做“增删改查”。而查是所有动作的前置条件——你连节点都拿不到,改谁?所以 DOM 查询能力的强弱,直接决定你写脚本的效率。
很多初学者容易陷入一个误区,就是把 jQuery 时代的 $ 选择器当作理所当然,换到原生 JavaScript 后反而不知道用什么。实际上现在的原生 API 已经非常完善,完全覆盖了绝大多数场景,而且性能不差,只是你不会用而已。
1.2 querySelector 与 getElement 两大家族的取舍
原生 DOM 查询大致分成两个流派。一派是 getElementById、getElementsByClassName 这些老牌 API,另一派是 querySelector、querySelectorAll 这两个后来的通用选择器。两者的核心区别在于:getElement 系接收的是“类型”参数,querySelector 系接收的是 CSS 选择器字符串。
从使用角度说,querySelector 系更灵活,因为你可以用任意 CSS 选择器语法,比如 .container > .item:first-child、input[name="username"],一次定位到深层元素。而 getElementById 只能按 id 查,getElementsByTagName 只能按标签查。所以新项目我几乎都用 querySelector 系。
但别急着放弃 getElement 系。我实测下来,在旧版本浏览器环境下,getElementById 的性能要比 querySelector('#id') 稍快一点,而且它返回的是 live 集合,后续 DOM 变化会自动更新。不过这点性能差异在现代浏览器里已经微乎其微,选型时优先考虑可读性和功能,而不是盲目追求“快”。
1.3 静态集合还是动态集合,先想清楚再动手
这是最容易踩坑的地方。getElementsByClassName、getElementsByTagName 返回的是 HTMLCollection,是动态的,而 querySelectorAll 返回的是 NodeList,在绝大多数情况下是静态的。什么意思?就是你通过 class 拿到的元素列表,如果后续往同一个 class 里追加了新元素,这个集合会自动多出新元素;而 querySelectorAll 的结果在调用那一刻就固定了,后续新增不会出现在快照里。
动态集合在少数场景下有优势,比如你要持续监视某个 class 下所有元素的变化,不用重新查询。但更多时候它带来的是困扰,因为你在循环里操作 DOM,可能因为集合长度不断变化导致死循环。所以我个人默认会用 querySelectorAll,除非明确需要动态特性,否则静态快照好排查得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 常用查询API逐个拆解
2.1 getElementById:最简单但限制最严
getElementById 只做一件事:按 id 精确匹配元素。它接收一个字符串,返回匹配到的第一个元素,或者没有匹配时返回 null。id 在 HTML 规范里是唯一的,所以这个方法通常返回一个元素而不是集合。
使用时有三个细节要注意。第一个是 id 大小写敏感,你必须精确匹配。第二个是如果 HTML 里有重复 id,虽然不合法,但浏览器不会报错,getElementById 会返回第一个匹配到的元素,这时候行为依赖 DOM 树顺序,容易出 bug。第三个是 id 中包含特殊字符时,比如 . 或 [],用 querySelector 语法需要转义,但 getElementById 直接传原始字符串就行,这也是它少有的优势之一。
javascript复制const box = document.getElementById('main-box');
if (box) {
box.style.backgroundColor = '#f0f0f0';
}
为什么一定要判断空?因为如果脚本执行时元素还没解析到,比如脚本放在 head 里,或者你拼写错了 id,box 就是 null,直接访问 box.style 会报 Cannot read properties of null。这算是新手最常见的运行时错误之一了。
2.2 getElementsByClassName:批量获取的动态陷阱
getElementsByClassName 接收一个或多个类名,返回 HTMLCollection。注意它匹配的是“包含所有这些类”的元素,而不是精确等于某个 class 属性。比如 document.getElementsByClassName('a b') 会匹配 class="a b" 也匹配 class="b a",只要同时有 a 和 b 就行。
这个 API 返回的是动态集合。我遇到过这么个真实问题:我想给一个容器里的所有 .item 加点击事件,用 getElementsByClassName 拿到列表后写了个 for 循环,循环里又往容器里插入了一个新 .item 节点。结果循环索引越界,因为列表长度在循环中变大了,而且新插入的节点是未绑定事件的。后来我改用 querySelectorAll,或者先把长度存下来,问题就清晰了。
javascript复制const items = document.getElementsByClassName('item');
for (let i = items.length - 1; i >= 0; i--) {
// 倒序遍历,动态集合增删时就安全得多
}
2.3 getElementsByTagName:标签查询的边界问题
getElementsByTagName 按标签名查询,同样返回动态 HTMLCollection。它有个有趣的行为:如果你传入 *,会返回所有元素节点,这在某些需要遍历全量节点的场景下很有用。
当你想找某个容器下所有 div 时,一般不会写 document.getElementsByTagName('div'),而是从具体容器开始找:
javascript复制const container = document.getElementById('container');
const divs = container.getElementsByTagName('div');
这里有个隐藏细节:如果容器本身也是 div,它会把自己也包含进去吗?不会。因为 getElementsByTagName 是从调用者元素的“后代”里搜索,不包含调用者自身。但如果你调用的不是元素而是
