获取div中的span元素:从原生JS到Vue/React的写法和排查指南
先说个结论这个需求我几乎每周都会在前端群里看到一次“获取div中的span的元素”听起来很简单但实际写起来坑非常多。不同的人问这句话背后的意思完全不一样——有人要拿div下面所有的span有人只要直接子元素有人要拿第一个还有人是要在Vue或者React的渲染函数里取节点。这篇文章就把这个看似基础的问题掰开揉碎把原生JS、jQuery、现代框架几种场景下的写法一次讲清楚顺便把我在实际项目里踩过的坑和排查思路整理出来。不管你是刚入门前端的新手还是写了两三年业务代码想查漏补缺的开发者这篇都值得花几分钟看完。我先说下这篇文章的核心思路。平时我们写DOM操作最怕的不是不会写API而是搞不清楚“我要的元素到底在哪一层”。你打开控制台手动去查元素一眼就能看到那个span代码写下去却返回空数组这种落差感最折磨人。所以这篇文章不止是给你几个选择器写法还会把“为什么这么写”“什么时候用哪种写法”讲清楚顺便帮你解决动态渲染、嵌套DOM、事件绑定等场景下的衍生问题。1. 先搞清楚你要“什么层级、什么数量”的span1.1 同一句话至少三种理解“获取div中的span的元素”这句话不同的人说出来代码写出来天差地别。我至少见过三种需求第一种我要获取div里面所有span元素。这是最宽泛的理解不管span藏在第几层嵌套里只要它的祖先节点里有那个div我都要。这时候你需要的是querySelectorAll(div span)或者getElementsByTagName(span)然后过滤父节点。第二种我只要div的第一层子元素里的span。也就是说 div 的直接子节点必须是 spanspan 里面怎么嵌套不关我事但 span 外面不能再包一层别的标签。这时候querySelectorAll(div span)就派上用场了注意那个号。第三种我其实只想拿第一个span。后面接按钮点击、做滚动定位、改样式反正只操作一次。这时候可以用querySelector(div span)它天生只返回第一个匹配项不用再[0]去取数组第一项。这三者的共同点是“div”和“span”这两个词都只是标签名你完全可以把它们替换成任何你实际项目里的类名、ID、属性选择器。但层级关系后代还是子元素不同选出来的结果可能差一个数量级。所以下次再写代码之前先问自己一句到底要哪一层这比急着百度API重要得多。1.2 为什么这个问题总有人问这个问题的热度一直居高不下背后有几个客观原因。第一CSS选择器和DOM API的认知断层。很多人在CSS里已经熟练使用div span这种写法但一到了JavaScript里就忘了可以直接把同一个选择器塞进querySelectorAll里用。还有人只知道getElementById和getElementsByTagName这两个老API压根不知道querySelector的存在。第二childNodes 和 children 的混淆。childNodes返回的是所有子节点——文本节点、注释节点、元素节点全都算children返回的才是子元素。如果你用childNodes去遍历然后想拿span你会发现返回结果里全是#text节点干扰项。这个我见过无数新手栽跟头。第三动态内容与静态内容的思维切换。传统页面里DOM是写死在HTML文件里的你什么时候去取都行。但现代前端项目里大部分DOM是JavaScript异步渲染出来的数据没到、v-for没跑完的时候去取得到的就是一个空列表。很多人以为自己的选择器写错了其实只是取早了。先把这个需求层面打通了下面所有写法才有意义。选择器只是工具思路才是核心。2. 原生JavaScript获取div中的span四种核心写法2.1 querySelectorAll日常开发最推荐直接写CSS选择器原生JS里最推荐的方式就是querySelectorAll它的参数直接就是CSS选择器字符串能把你在样式表里的知识直接搬到JavaScript里。// 获取div下所有span所有后代层级 const allSpans document.querySelectorAll(div span); // 获取div下的直接子元素span const directChildSpans document.querySelectorAll(div span); // 获取div下的第一个spanquerySelector只返回第一个匹配 const firstSpan document.querySelector(div span); // 获取类名为content的div下面的span const contentSpans document.querySelectorAll(div.content span);这里有几个关键点要额外强调。第一querySelectorAll返回的是NodeList虽然它支持forEach遍历但它不是真正的数组push、pop、filter、map这些数组方法直接用不了。如果你要调用数组方法先Array.from()转成数组或者用扩展运算符[...nodeList]转一下。const spans Array.from(document.querySelectorAll(div span)); const spanTexts spans.map(item item.textContent);第二参数必须是一个合法的CSS选择器。你直接传div span没问题但如果选择器本身拼错了比如多了一个括号、少了一个引号浏览器会直接抛SyntaxError异常代码就断了。动态拼接选择器字符串时我建议用模板字符串并且在外层加try-catch兜底。第三这个API相比之下“快”得没有悬念。虽然每次调用都会重新从根节点开始查找但对于绝大多数业务页面那点DOM节点数量来说性能差异你根本感知不到。2.2 getElementsByTagName性能最稳的老牌API但注意动态集合如果你是老一代前端可能更习惯用getElementsByTagName(span)。这个API在性能上其实一点都不差而且写法上也有它的独到之处。// 先拿到div再拿div内部所有span标签 const div document.getElementById(wrapper); const spans div.getElementsByTagName(span); // 如果不确定div只有一个加个索引 const firstDiv document.querySelectorAll(div)[0]; const spans2 firstDiv.getElementsByTagName(span);这个方法最大的特点是返回结果是HTMLCollection它是动态集合。什么意思如果你先在变量里存了这个集合然后往DOM里又新增了一个span再访问这个集合变量它会把新加入的span也算进去。听起来好像很智能但实际业务里这恰恰容易出问题——你遍历这个集合的时候如果循环体里顺带删除了某个span集合长度会实时变化遍历下标直接错乱很容易跳过元素或者出现索引越界。// 反面教材动态集合一边遍历一边删除会漏删 const spans div.getElementsByTagName(span); for (let i 0; i spans.length; i) { if (someCondition) { spans[i].remove(); // 删除后length变了i还继续自增旁边的元素被跳过了 } }所以如果你确定拿到的元素集合在遍历期间不会增删用getElementsByTagName没问题但如果你要动态操作DOM我建议改成querySelectorAll加Array.from的方式因为querySelectorAll返回的是静态快照遍历过程中再怎么增删元素集合本身不会变。2.3 children 标签名过滤只拿第一层直接子元素如果你要的是“div的直接子级span”而且div内部结构比较复杂用children配合条件过滤是最直观的写法。const div document.getElementById(wrapper); const children div.children; const directSpans []; for (let i 0; i children.length; i) { if (children[i].tagName SPAN) { directSpans.push(children[i]); } } // 更简洁的过滤写法 const directSpans2 Array.from(div.children).filter( child child.tagName SPAN );注意细节tagName返回的标签名是大写的所以比较时要么写SPAN要么用child.tagName.toLowerCase() span。很多人在这里漏了大小写转换结果怎么都匹配不上找了半天才发现是大小写的问题。这种做法虽然代码行数多一些但非常灵活。比如你想过滤“带有某个class的span”只需把过滤条件改成child.classList.contains(highlight)。如果嵌套层级很多querySelectorAll(div span)一条选择器就搞定了更省事但如果你要在循环里对每个子元素做多条件判断children加数组方法才是更好的选择。2.4 用querySelector直接定位第一个span最短路径很多场景下你根本不需要一个集合只想要第一个span的文本或者位置。用querySelector就够。// 第一个匹配的span的元素 const firstSpan document.querySelector(div span); if (firstSpan) { console.log(firstSpan.textContent); }这里有一个必须养成的习惯用querySelector之前一定要判空。因为如果不匹配任何元素它返回的是null你要是直接firstSpan.textContent就会抛TypeError。写代码的时候养成“取到元素先判断有没有”的习惯能帮你避免一大半生产事故。刚才这些方法各有利弊我整理了一张对照表方便你在写代码前快速选型。需求场景推荐写法返回值类型备注获取div下所有后代spanquerySelectorAll(div span)NodeList静态最常用支持forEach只拿div的直接子级spanquerySelectorAll(div span)NodeList静态CSS选择器一步到位拿第一个spanquerySelector(div span)Element或null必须判空老项目/需要动态集合特性getElementsByTagName(span)HTMLCollection动态注意遍历时增删问题复杂过滤条件Array.from(div.children).filter(...)普通数组灵活可配合类名判断2.5 性能与可读性的平衡很多人会纠结这三种方式谁性能更好。实际上在普通页面里节点树撑死几千个节点querySelectorAll和getElementsByTagName的差距是毫秒甚至微秒级别的你根本感知不到。真正影响性能的不是API本身而是调用次数——如果你在滚动事件里频繁去查DOM好用的API也白搭。我的习惯是如果在循环或者高频事件里确实需要频繁查询某个固定集合先把它存到变量里缓存起来如果只是在某个事件回调里查一次直接用querySelectorAll就行完全不用思考性能问题。3. 用了jQuery和现代框架怎么拿span元素3.1 jQuery三件套find、children、first虽然新项目基本都放弃jQuery了但存量老项目、后台管理模板、WordPress主题里jQuery仍然很常见。jQuery选择器和CSS几乎一致写起来很顺手。// 获取div内所有span包含子级 $(div span); // 获取div下直接子级span $(div span); // 获取第一个span $(div span).first();jQuery里有一个很重要的区分find()和children()。find(span)会往下找所有层级的span相当于querySelectorAll(div span)children(span)只找直接子级相当于querySelectorAll(div span)。// 如果已经有一个jQuery对象 const $div $(#wrapper); // 所有后代span const allSpans $div.find(span); // 直接子元素里的span const directSpans $div.children(span); // 拿到第一个span的DOM原生对象 const firstSpanDom $div.find(span)[0];另外jQuery的.first()是个好用的方法它在匹配集合里取第一个元素并包装成jQuery对象如果你要原生DOM节点记得加[0]或者用.get(0)。使用jQuery的时候有一个坑不得不说——如果你用$(div span)这种全局选择器就会匹配页面上所有div里的所有span很可能包括了你不想操作的其他区域。我建议不管写原生JS还是jQuery都尽量给目标div加一个id或者特定类名用$(#wrapper span)替代$(div span)避免误伤。3.2 Vue和React场景下的特殊处理现在的主流项目里Vue和React这些框架主导了DOM的生成操作DOM的方式也和传统前端不太一样。在Vue项目里最常见的需求是“在某个方法里拿到当前组件里那个span”。我推荐用ref去标记元素再用this.$refs获取。template div refwrapper span classlabel价格/span span classvalue{{ price }}/span /div /template script export default { methods: { handleClick() { const wrapper this.$refs.wrapper; // 这时你可以用前面讲过的任何DOM方法 const spans wrapper.querySelectorAll(span); } } } /script这里有一个Vue新手必踩的坑Vue的DOM更新是异步的。你改了price这个响应式数据马上用$refs去摸DOM拿到的往往还是旧的。此时需要用this.$nextTick()等DOM更新完成后再取值。this.price 999; this.$nextTick(() { const wrapper this.$refs.wrapper; const spans wrapper.querySelectorAll(span.value); console.log(spans[0].textContent); // 这时候是新值 });React场景下主流做法是使用useRef绑定节点在useEffect里访问DOM。import { useRef, useEffect } from react; function PriceCard() { const wrapperRef useRef(null); useEffect(() { const wrapper wrapperRef.current; const spans wrapper.querySelectorAll(span); // 在这里做初始化操作 }, []); return ( div ref{wrapperRef} span价格/span span100元/span /div ); }很多人在非事件回调的地方比如直接在组件函数体里试图访问wrapperRef.current得到的几乎肯定是null。因为DOM还没挂载ref自然为空。一定要在useEffect里或者在某个事件回调里再去操作。4. 结合业务场景说几个真实用法4.1 动态渲染后获取必须等DOM ready这个场景太典型了我们拿“接口返回数据后渲染列表再操作列表里的span”来说。async function loadData() { const list document.getElementById(list); const data await fetch(/api/list).then(res res.json()); // 假设这里用innerHTML把data渲染进了list里 list.innerHTML data.map(item span${item.name}/span).join(); // 必须在这个位置获取span因为此时DOM才真正有了渲染结果 const spans list.querySelectorAll(span); spans.forEach((span, index) { span.addEventListener(click, () { console.log(你点击了第, index, 个span); }); }); }这里面有两个细节值得注意。第一个如果接口还没返回就调用querySelectorAll返回长度一定是0这不是你的选择器写错了。第二个渲染大列表的时候如果用forEach给每个span都绑一个事件会产生大量事件监听器性能上不划算。更好的做法是事件委托——只给list这个父容器绑一次click事件然后通过event.target判断用户是不是点到了span。list.addEventListener(click, function (event) { const target event.target; if (target.tagName SPAN) { console.log(span内容, target.textContent); // 还可以通过 dataset 拿到额外信息 console.log(自定义属性, target.dataset.index); } });这种做法的好处是无论列表渲染多少项监听器始终只有一个而且即使后续又向列表里插入了新的span这个监听逻辑也照样生效。4.2 处理复杂嵌套类名 data属性的组合约束业务页面里的DOM往往嵌套很多层div套divspan套span。如果只靠div span这种粗粒度选择器很容易把不相关的span一起捞出来。这时候我强烈建议在组件的HTML结构上提前做好“标记”。div classproduct-card div classproduct-title span classname无线耳机/span /div div classproduct-price span classsymbol¥/span span classprice-number199/span /div div classproduct-tags span classtag新品/span span classtag限时优惠/span /div /div如果你只想拿价格数字用.product-price .price-number这个选择器远比div span精确得多。甚至可以用属性嵌入业务标识span classprice-number>document.querySelectorAll([data-roleprice]);这样即使以后类名重构了只要data-role属性还在选择器依然可靠。在团队项目里这种“属性约定”比“类名约定”更抗变化因为类名经常被CSS重构牵连。4.3 拿完元素之后做什么常见的三个后续操作拿到span之后业务上无外乎三种后续操作。改内容。用textContent或者innerHTML。注意如果内容里包含用户输入或者接口返回的富文本直接用innerHTML会有XSS风险能讲清楚这个问题的开发者面试官通常都会高看一眼。普通文本一律用textContent。span.textContent 新的文本; // 安全 // span.innerHTML b富文本/b; // 谨慎使用改样式。直接操作style属性span.style.color #f00; span.style.fontWeight bold;加类名。多用classList而不是直接改className因为className会覆盖原有类名。这里要注意别忘记classList.add和classList.remove的兼容性——不过现在主流浏览器都支持了基本可以放心使用。span.classList.add(active); span.classList.remove(hidden);5. 常见问题与排查技巧实录5.1 问题速查清单下面这些场景是我在前端交流群里看到最多的求助帖我整理成了一张清单你遇到类似问题时可以直接对照排查。现象根本原因解决办法document.querySelectorAll(div span)返回空NodeList页面里的div和span之间还有CDATA、模板注释节点或者div自己都没有匹配上先确认document.querySelector(div)能不能返回div再看层级关系是不是后代拿到的span数量比页面上肉眼看到的少有些span是JS异步渲染的调用代码执行时还没生成把获取操作挪到渲染之后或者放到setTimeout/nextTick里拿到了嵌套在子div里的span只想要直接子级用了div span后代选择器改成div span或者用children过滤用childNodes遍历时出现大量#textchildNodes包含纯文本节点改用children或者判断nodeType 1控制台能查出来代码里查出来却是null代码执行时间点在DOM渲染之前放到DOMContentLoaded回调内或脚本移到body末尾jQuery里$div.children(span)取不到东西给children传了选择器但直接子元素根本没有span标签span全在下一层按需改用find遍历getElementsByTagName结果时漏元素动态集合长度实时变化一边遍历一边删导致索引错位转静态数组Array.from后再操作Vue项目里this.$refs.wrapper是undefined元素使用了v-if且条件为false或者ref被写在了v-for循环上确认条件渲染已为truev-for循环的ref需要用函数形式5.2 分享两个排查心态排查“获取不到元素”这类问题的时候我建议不要盯着代码死看而是先打开浏览器开发者工具在Console里手动执行一遍你的选择器。这样做能立刻告诉你两件事这个选择器本身到底能不能匹配到元素以及当前DOM状态到底是什么。有时候你写了document.querySelectorAll(div span)在Console里一敲返回了多个元素说明选择器没问题但是你的JS执行时机太早运行到这一步时DOM还没渲染完。这种情况把代码挪到页面加载完成后又能解决一批。反过来如果Console里也返回空那就是选择器没写对或者层级理解有偏差回到第一章节的思路再拆解一遍。从个人经验来说我写“获取div里的span”这类代码时九成场景直接用querySelectorAll一条CSS选择器就能解决。真正让我踩坑的反而是在Vue项目里忘了nextTick、在动态渲染时过早取节点这类“时机问题”。还有一个我最近才彻底想明白的小经验拿到元素之后操作前先判断它在不在当前视口、是不是隐藏状态。有时候不是取不到而是取到了但充满了“看不见”的坑比如高度为0、被遮挡、或者出现在了不可见区域。尤其配合“vue div超出隐藏”这类场景元素的offsetParent为null时代表它或者它的某个父级设了display:none这种情况你拿到手也白拿操作结果用户根本看不到排查起来比“取不到”更要命。后面大家写代码时可以多留一个心眼拿到元素集合后先打印看看它的长度和第一条内容有没有意义而不是直接链式操作。确认你站在了正确的DOM上再往下写逻辑这样排查时间能省一半。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →