ARTICLE DETAIL

资讯详情

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

原生DOM获取元素的六种方式:全解析与对比

原生DOM获取元素的六种方式:全解析与对比 每次看到刚入门的朋友拿着 jQuery 或者各种框架的$refs、ref找元素找得飞起我就想问一句原生document对象的那几个获取元素方法你当真玩明白了不是说你非得用原生而是框架封装再多底层跑的还是浏览器那六个原生方法。我把话放这儿把这六种方式吃透你排查 DOM 相关 bug 的速度至少快一倍看框架源码也不至于一头雾水。这六种方式分别是getElementById、getElementsByClassName、getElementsByTagName、getElementsByName、querySelector、querySelectorAll。前四个是老一辈的直给后两个是 CSS 选择器时代的精准打击。它们之间到底什么区别什么时候用哪个为什么有的返回HTMLCollection有的是NodeList为什么有的实时更新有的静态快照这篇文章一次给你掰扯清楚。适合刚入门前端的新人打基础也适合写了两三年业务但一直没系统梳理过的同学查漏补缺。1. 先把概念捋顺document、节点、页面元素到底是什么关系1.1 别把节点和元素混为一谈咱们老说DOM 节点页面元素听起来差不多但在浏览器眼里完全是两码事。整个 HTML 页面加载完之后浏览器会把它解析成一棵倒挂的树这棵树叫 DOM 树。树上每一个分叉、每一片叶子都是节点。节点有好多类型元素节点、文本节点、属性节点、注释节点甚至换行符在某些场景下也会被当成文本节点。你看到的div、p、button这些标签是元素节点。元素里面的文字 Hello World是一个文本节点。标签上的classbox、idapp是属性节点。!-- 注释 --是注释节点。而我们平时说的获取页面元素绝大多数情况指的是元素节点不是文本节点更不是注释节点。1.2 document 对象到底是干嘛的document对象就是这棵 DOM 树的根节点它代表了整个 HTML 文档。你可以把它理解成一个总入口——所有对页面元素的查找操作几乎都得通过document或者某个元素节点来发起。为什么强调这一点因为后面你会发现getElementById只能从document上调用而getElementsByTagName、querySelectorAll还能从任意元素节点上调用这个差异直接影响你怎么组织代码。1.3 为什么从 document 开始找元素这么重要说个真实场景你拿到一个别人写的页面想知道页面上一共有多少个输入框。如果你只会document.getElementById(input)逐个拿遇到没有id的输入框就傻眼了。反过来你要是知道document.getElementsByTagName(input)能一把梭全部拿回来再配合.length数个数效率完全不一样。所以搞清楚从document出发有哪些管道通向元素是 DOM 操作的基本功也是后面学事件委托、虚拟 DOM 比对的基础。2. 六种获取方式逐个拆解语法、返回值、适用场景先列一个总览表心里有个底然后再一个个说细节。方法参数形式返回类型返回数量是否实时兼容性getElementById字符串 IDHTMLElement 或 null单个—全支持getElementsByClassName字符串类名可多个HTMLCollection多个是IE9getElementsByTagName字符串标签名或 *HTMLCollection多个是全支持getElementsByName字符串 name 属性NodeList多个部分全支持querySelectorCSS 选择器字符串HTMLElement 或 null单个—IE8部分querySelectorAllCSS 选择器字符串静态 NodeList多个否IE8部分2.1 getElementById最直接但限制也最明确代码写起来就是一行const box document.getElementById(app);注意几点参数不加#直接传 ID 字符串。新手最容易犯的错就是写document.getElementById(#app)返回null白查半天。返回的是HTMLElement或者null。如果页面里没有这个 ID返回null所以后续操作前最好判空不然会报Cannot read properties of null。HTML 规范里要求 ID 在文档中唯一但浏览器很宽容如果页面里真有俩元素 ID 相同这个方法返回第一个匹配的元素后面的被无视了。平时工作里该用唯一 ID 还是得用别想着靠浏览器兜底。2.2 getElementsByClassName类名查找的利器但小心它是活的语法const items document.getElementsByClassName(item);几个细节值得说返回的是HTMLCollection也就是一个类数组对象。它不是数组所以你不能直接.map()、.filter()得先转数组或者用Array.prototype.slice.call()。HTMLCollection是实时集合。这意味着如果你拿到一个集合之后往 DOM 里再添加一个带item类的元素这个集合的length会自动变长。这个特性在某些场景下很爽但在循环里边遍历边删元素时会坑到你怀疑人生。后面专门讲这个坑。参数可以传多个类名用空格分隔比如getElementsByClassName(box red)浏览器要求元素同时具备这两个类才匹配顺序无关。注意是同时具备不是或的关系。2.3 getElementsByTagName全标签扫描还可以用通配符语法const divs document.getElementsByTagName(div);参数不区分大小写HTML 文档里你写getElementsByTagName(DIV)也没问题。支持通配符*document.getElementsByTagName(*)会返回页面里所有元素节点这个在某些遍历整棵树的场景里非常好用。同样返回实时HTMLCollection。它不只能从document上调还能从任意元素节点上调比如container.getElementsByTagName(p)只找容器内部的段落性能上比从全局找要快一点语义也更清晰。2.4 getElementsByName表单场景里的隐藏高手语法const radios document.getElementsByName(gender);返回的是NodeList旧浏览器里是HTMLCollection主要用于表单元素尤其是单选框、复选框这种有相同name的分组。因为一个页面里可以有很多元素叫同一个name所以它天生是多选的。关键坑点不是所有元素都有name属性div、p这些没有name属性的标签这个方法抓不到。所以它的使用场景基本固定在表单那一亩三分地。2.5 querySelector用 CSS 选择器精准定位第一个语法const firstBtn document.querySelector(.btn-primary);这是我最常用的方法没有之一。因为 CSS 选择器的表达能力太强了一个选择器能顶前面好几个方法。比如document.querySelector(ul.list li.active); document.querySelector([data-id123]); document.querySelector(input[typetext]:nth-of-type(2));这些都是前面那几个方法很难一句写出来的。注意点返回第一个匹配的元素没有匹配就返回null。不能选择伪元素比如::before、::after因为伪元素不是真实 DOM 节点。如果选择器写错了格式会直接抛SyntaxError异常而不是返回null。2.6 querySelectorAll批量获取 静态快照语法const buttons document.querySelectorAll(button);它的返回值是静态 NodeList。什么叫静态意思是你获取之后哪怕后面又删了一个button或者加了一个button这个集合不会变。这一点和getElementsByTagName的实时HTMLCollection形成鲜明对比。NodeList和HTMLCollection还有一个区别新式浏览器里NodeList支持forEach方法HTMLCollection不支持虽然可以通过Array.prototype.forEach.call强行用。所以你要是想写函数式风格的代码querySelectorAll舒服得多。3. HTMLCollection 和 NodeList 的区别这坑值得单独拉出来讲3.1 实时集合 vs 静态快照用错场景就出 bug上面提到了两个核心差异实时性、可用的遍历方法。再展开说说实时性的实际影响。假设你要把页面上所有li元素一个一个删掉。用getElementsByTagName(li)const list document.getElementsByTagName(li); for (let i 0; i list.length; i) { list[i].remove(); }你以为删完了实际只删了一半。原因就是list是实时的你删掉第一个后面的元素下标自动往前挪length也变小了。循环到一半的时候i已经追上了length循环就停了。换成querySelectorAll(li)就没这个问题因为它是静态快照length固定不变怎么遍历都安全。3.2 怎么把类数组转成真数组平时用querySelectorAll拿到的集合想用map、filter、reduce这些数组方法有三种常见转法// 方法一展开运算符 const arr [...document.querySelectorAll(div)]; // 方法二Array.from const arr2 Array.from(document.querySelectorAll(div)); // 方法三slice 大法 const arr3 Array.prototype.slice.call(document.querySelectorAll(div));这三种都能用前两种是 ES6 之后的写法第三种是 ES5 时代的兼容写法。我个人偏好Array.from因为后续还能接第二个参数做映射处理比如Array.from(list, el el.textContent)直接拿到所有文本内容。4. 性能对比与选型别再随手写一堆重复查询了先看性能。大家可能觉得querySelector这么全能性能是不是特别差实测结论是在真实页面里差距没有想象中那么大但确实有规律可循。方法大致性能评分备注getElementById最快引擎内部有 ID 索引表直接命中getElementsByTagName快标签种类有限扫描成本低getElementsByClassName较快现代引擎做了类名索引优化getElementsByName中等依赖属性映射表querySelector / All相对较慢需要先解析 CSS 选择器再匹配复杂选择器成本更高我实际项目里的经验是高频操作比如requestAnimationFrame里每帧查元素就别写复杂选择器了能用getElementById就先拿个根节点然后用querySelector在小范围内找。但日常大多数业务代码随便写性能瓶颈根本不在 DOM 查询上。4.1 选型决策树什么场景用哪个简单总结一下我的选型逻辑页面只有一个元素且它有唯一 ID直接getElementById没毛病。需要按 CSS 选择器精准定位只要一个querySelector。需要拿同类型的批量元素比如所有div、所有inputgetElementsByTagName或querySelectorAll。需要按多个类名组合筛选getElementsByClassName(a b)或querySelectorAll(.a.b)。表单里拿同 name 的 radio/checkbox 分组getElementsByName。需要在遍历过程中动态反映 DOM 变化用实时集合getElementsByTagName。需要快照稳定遍历并配合数组方法用querySelectorAll。4.2 缓存查询结果的习惯要养成不管用哪个方法只要元素不是动态反复增删的就应该把查询结果缓存到变量里别在函数里一遍遍查。// 不推荐 function updateTitle() { document.querySelector(.title).textContent hello; document.querySelector(.title).style.color red; } // 推荐 const titleEl document.querySelector(.title); function updateTitle() { titleEl.textContent hello; titleEl.style.color red; }5. 实战中的常见坑我踩过的希望你绕过去5.1 在循环里边改 DOM 边用实时集合元素被跳过这个上面提过一半再补充一个变体不只删除会出问题新增也会。const items document.getElementsByClassName(item); // 实时集合 for (let i 0; i items.length; i) { const clone items[i].cloneNode(true); document.body.appendChild(clone); // 集合变长循环可能失控 }这种边遍历边增加的操作会让length一直变循环次数完全不可控。解决思路也很简单要么转成静态快照要么倒序遍历。5.2 querySelector 里 ID 以数字开头必须转义CSS 选择器里数字开头的类名或 ID 需要特殊处理。比如有一个id123abc的元素你不能直接写document.querySelector(#123abc); // 报错需要转义document.querySelector(#\\31 23abc); // CSS 转义写法这写法太反人类了日常遇到这种命名建议直接换getElementById(123abc)省心。所以从侧面也说明一个编码规范问题HTML 里的 ID 和类名尽量别用数字开头不是不能用是后续选择器处理太痛苦。5.3 动态渲染的内容脚本执行早于渲染就取不到这在原生 JS 写页面时非常常见。script标签放在head里然后立刻去查body里的元素必然返回null。解法是等 DOM 加载完再执行常见三种// 方法一放在 body 标签末尾 // 方法二DOMContentLoaded document.addEventListener(DOMContentLoaded, () { // 查询操作放这里 }); // 方法三defer 属性针对脚本文件很多新手调试半天代码没毛病结果就是执行时机不对。5.4 getElementsByClassName 的多类名是与不是或再强调一次因为它从字面上太容易误解了。// 要求元素同时有 classbox red 才能匹配到 document.getElementsByClassName(box red); // 要匹配任意一个得分开写再合并classbox red能匹配classbox blue不能匹配classred green也不能匹配。如果你真想实现或的逻辑要么分两次查再自己去重要么用querySelectorAll(.box, .red)。5.5 某些表单元素的 name 会被老版本 IE 当成 id 用这算一个历史遗留坑在 IE6/IE7 里getElementById会顺手匹配name属性。这意味着你页面里有个input namesubmit然后你document.getElementById(submit)在老 IE 里可能拿到那个 input。现在基本没人管 IE 了但遇到远古代码还是要心里有数。6. 一个综合小案例把六种方法用在同一页面光讲不练不行我写一个实际页面片段把这六种方法全用上。form idloginForm div classform-group label用户名/label input typetext nameusername classinput-field / /div div classform-group label手机号/label input typetel namephone classinput-field / /div div classform-group label性别/label input typeradio namegender valuemale / 男 input typeradio namegender valuefemale / 女 /div button typesubmit classbtn btn-primary登录/button /form对应的查询操作// 1. 拿整个表单 const form document.getElementById(loginForm); // 2. 拿所有输入框假设 UI 上输入框都有 input-field 类 const inputs document.getElementsByClassName(input-field); // 3. 拿所有 input 标签 const allInputs document.getElementsByTagName(input); // 4. 拿性别单选框分组 const genderInputs document.getElementsByName(gender); // 5. 拿第一个带有 btn-primary 类的元素 const primaryBtn document.querySelector(.btn-primary); // 6. 拿所有按钮包括 submit、reset 等 const allBtns document.querySelectorAll(button, input[typesubmit]);这时候你就能直观看到六种方法各自的脾气了有的拿单数有的拿复数有的是实时的有的是静态的有的只能在document上用有的还能在form元素上继续缩小范围比如const usernameInput form.querySelector(input[nameusername]);从form上调用querySelector查找范围就锁定在表单内部不会误伤页面其他地方的同名元素代码语义也更清楚。7. 结尾一点个人体会写了这么多年原生 JS我越来越觉得document的这六个方法不是竞争关系而是互补关系。真正的高手不是只背querySelectorAll一把梭而是清楚知道每个方法的边界和特性——实时还是静态单数还是复数能不能链式调用在哪个节点上调用更有意义。遇到动态渲染的列表我会考虑用getElementsByTagName的实时特性做数据监听遇到复杂的状态切换界面我用querySelectorAll拿快照再统一处理。这些判断不靠记 API靠的是踩坑之后形成的肌肉记忆。你要是刚学建议把这篇文章里的代码都自己在控制台跑一遍尤其是循环里删元素那个例子亲测一次比看十篇博客都管用。
返回列表