核心内容摘要
ExplorerPatcher:重塑Windows工作环境的全方位优化指南
端菜鸟别再乱用getElement了querySelector全家桶真香指南附避坑技巧前端菜鸟别再乱用getElement了querySelector全家桶真香指南附避坑技巧说实话我刚开始写前端那会儿真的是个铁憨憨。
那时候觉得JavaScript嘛不就是document.getElementById(‘xxx’)走天下直到有一天我把代码提交上去我们组那个秃顶的老大哥看了一眼露出了一种这孩子没救了的表情慢悠悠地说“兄弟现在都2024年了你还在用上个世纪的API啊”我当时那个脸红的恨不得找个地缝钻进去。
后来我才明白原来现代浏览器早就给我们准备了大杀器——querySelector和querySelectorAll。
这俩兄弟简直就是DOM操作界的瑞士军刀用起来比getElementById爽太多了。
但是吧爽归爽坑也是真的多。
今天我就把这些年踩过的坑、吃过的亏还有发现的一些骚操作全部倒给你们听听。
querySelector到底是个啥玩意儿先说说这个querySelector到底是何方神圣。
简单来讲它就是浏览器给咱们开的一个后门让你在JS里面直接用CSS选择器的语法来找元素。
以前你要找个东西得记一堆APIgetElementById、getElementsByClassName、getElementsByTagName还得记哪个返回的是单个元素哪个返回的是HTMLCollection哪个会实时更新哪个不会烦都烦死了。
但是querySelector不一样它就像一个万能钥匙。
你想啊咱们写CSS的时候是怎么选元素的.class选类名#id选ID[attr]选属性甚至:hover、:nth-child这种伪类都能往上招呼。
querySelector就是让你在JS里用完全一样的语法。
这意味什么意味着你不用再背两套语法了CSS怎么写JS就怎么写大脑不用来回切换爽不爽我记得第一次用上它的时候那种感觉就像是从诺基亚换成了iPhone。
以前我要选一个class叫active的按钮得写document.getElementsByClassName(‘active’)[0]还得担心如果页面上没有这个class会不会报错。
现在呢直接document.querySelector(‘.active’)简洁得令人发指。
而且如果页面上没有这个元素它也不会报错就是返回null这点比getElementById还温柔。
不过这里我得插一嘴虽然querySelector很万能但它也不是没有脾气的。
它只返回第一个匹配的元素注意是第一个。
哪怕你页面上有十个class都是item的div它也只会给你第一个。
这点很多人刚开始用的时候容易懵以为会像getElementsByClassName那样返回一堆结果拿到手发现只有一个然后就开始怀疑人生“我明明写了十个啊怎么只有一个”querySelector和querySelectorAll这对卧龙凤雏说到querySelectorAll这货跟querySelector是亲兄弟但性格完全不一样。
如果说querySelector是独狼那querySelectorAll就是海王——它要把所有匹配的元素全部收入囊中。
它返回的是一个NodeList看起来像个数组用起来也像个数组有length属性也能用方括号取值但它其实不是真正的数组。
这点我必须要重点强调因为这个问题坑了我整整一个下午。
那时候我刚入行拿到了querySelectorAll返回的结果心想这不就是个数组嘛直接上map操作结果浏览器控制台啪啪给我两巴掌“map is not a function”。
我当时就懵了什么情况这不是明明有length吗怎么不能map后来查文档才知道NodeList是类数组或者说伪数组。
它只有一些基本的数组特性但没有数组的那些高级方法比如map、filter、reduce这些通通没有。
你要想用这些方法得先把它转换成真正的数组。
怎么转最优雅的方式是用Array.from()比如Array.from(document.querySelectorAll(‘.item’)).map(…)。
或者用ES6的扩展运算符也行[…document.querySelectorAll(‘.item’)].filter(…)。
还有一个巨大的区别querySelector返回的是单个Element对象或者null而querySelectorAll返回的是NodeList。
NodeList这玩意儿还有个特性大部分情况下它是静态的static也就是说你在调用querySelectorAll的那一刻浏览器就把当时匹配的所有元素快照下来了。
后面即使你在DOM里新增了符合条件的元素这个NodeList也不会自动更新。
这跟getElementsByClassName返回的HTMLCollection不一样HTMLCollection是活的会实时反映DOM的变化。
这个特性有好有坏。
好处是你不用担心遍历到一半的时候列表突然变了坏处是如果你指望它能自动感知新元素那就得失望了。
我有一次写了个无限滚动列表用querySelectorAll拿了所有的卡片然后往下滚动加载更多发现新加载的卡片怎么都选不到排查了半天才发现原来是静态快照的问题。
实际开发中怎么用才不翻车好了理论说了一堆咱们来点实际的。
在日常开发中这俩API到底该怎么用才能避免翻车首先最常见的一个场景就是给一类元素绑定事件。
比如说你页面上有一堆按钮class都叫btn-action你想给它们都加上点击事件。
以前你可能会想到getElementsByClassName然后写个for循环。
现在用querySelectorAll配合forEach代码简洁得不像话// 获取所有操作按钮constbuttonsdocument.querySelectorAll(.btn-action);// 直接forEach遍历不用写for循环了buttons.forEach((btn,index){btn.addEventListener(click,(e){// 这里可以拿到当前点击的按钮console.log(你点了第${index1}个按钮,e.target);// 加个点击效果比如闪烁一下btn.style.transformscale(
0.
;setTimeout((){btn.style.transformscale(
;},
;});});看起来很美对吧但是这里有个大坑如果你的这些按钮是动态生成的比如通过AJAX加载进来的或者用户点了加载更多才出现的那上面这段代码就会失效。
因为querySelectorAll只会在执行的那一刻去查找DOM那时候这些按钮还不存在呢当然就找不到了。
这种情况怎么办我见过很多新手会犯一个错误在每次动态加载完内容后重新执行一遍上面的代码。
这确实能解决问题但是效率极低而且如果之前已经绑定过事件的按钮会重复绑定导致点一次触发好几次。
正确的姿势应该是事件委托Event Delegation// 假设这些按钮都装在一个id为container的父元素里constcontainerdocument.querySelector(#container);// 只在父元素上绑一个事件监听器container.addEventListener(click,(e){// 检查点击的是不是我们要的按钮closest会向上查找最近的匹配元素constbtne.target.closest(.btn-action);// 如果点到的不是按钮或者点在按钮的子元素上但没命中按钮本身btn就是nullif(!btn)return;// 确定是我们要的按钮处理业务逻辑console.log(按钮被点了:,btn.dataset.action);// 还可以拿到这个按钮是第几个相对于所有同类按钮constallButtonscontainer.querySelectorAll(.btn-action);constindexArray.from(allButtons).indexOf(btn);console.log(这是第${index}个按钮);});看到没有这里我又用了一次querySelectorAll但只是为了获取index真正的事件绑定只需要一次。
而且哪怕后面再新增一百个按钮这个事件监听照样管用因为点击事件会冒泡到父元素。
还有一个特别常见的翻车现场在DOM还没加载完的时候就急着去查元素。
比如说你把script标签放在了head里然后里面写了document.querySelector(‘#app’)这时候HTML body都还没解析呢当然查不到返回null。
后面你再对这个null操作直接报错Cannot read property ‘xxx’ of null。
解决办法很简单要么把script放到body最后要么用DOMContentLoaded事件// 安全的做法等DOM完全加载后再查询document.addEventListener(DOMContentLoaded,(){constappdocument.querySelector(#app);// 这时候肯定能查到可以安心操作app.innerHTMLh1页面加载完成/h1;});// 或者用async/defer属性如果是外部脚本的话那些年我们一起掉过的坑说到坑我真的有一肚子苦水要倒。
querySelector虽然好用但有些坑深不见底掉进去半天爬不出来。
第一个大坑iframe。
如果你要操作iframe里的元素直接在父页面用document.querySelector是查不到的因为那是另一个document。
必须得先拿到iframe的contentDocument或者contentWindow.document。
而且这里还有个安全限制如果iframe加载的是跨域的内容你直接访问contentDocument会报跨域错误浏览器会把你拒之门外。
// 错误示范直接在父页面查iframe里的元素constiframeBtndocument.querySelector(.iframe-internal-btn);// 永远返回null// 正确姿势constiframedocument.querySelector(#myIframe);try{constiframeDociframe.contentDocument||iframe.contentWindow.document;constbtniframeDoc.querySelector(.iframe-internal-btn);// 现在才能操作iframe里的按钮btn.addEventListener(click,(){console.log(iframe里的按钮被点了);});}catch(e){console.error(跨域了兄弟访问不了iframe内容,e);}第二个坑伪类选择器。
很多人是从jQuery转过来的习惯了用:contains(‘文字内容’)这种选择器。
但是在原生querySelector里这个是不支持的我当时就试过document.querySelector(‘div:contains(“提交”)’)结果控制台直接抛错说这是个无效的选择器。
原生CSS选择器支持的伪类有限像:contains、:eq、:gt这些jQuery的私货在querySelector里统统不能用。
// 这行代码会直接报错// const btn document.querySelector(button:contains(保存));// 替代方案先查出来再filterconstbuttonsdocument.querySelectorAll(button);consttargetBtnArray.from(buttons).find(btnbtn.textContent.includes(保存));第三个坑特殊字符转义。
这个坑我踩过一次记忆深刻。
那时候我在做一个后台管理系统ID是根据数据库主键生成的比如item:
user:456这种格式中间带了个冒号。
然后我写了document.querySelector(‘#item:123’)结果死活查不到控制台还报错。
后来才明白在CSS选择器里冒号是有特殊含义的伪类所以ID或者class里如果包含了冒号、点号、方括号这些特殊字符必须要用反斜杠转义。
而且因为在JS字符串里反斜杠本身也要转义所以要写两个反斜杠。
// 假设有个元素的id是nav:user:profile// 错误写法直接写会解析失败// const el document.querySelector(#nav:user:profile);// 正确写法特殊字符前加双反斜杠因为在JS字符串里\要写成\\consteldocument.querySelector(#nav\\:user\\:profile);// 更保险的做法用CSS.escape方法现代浏览器支持constidnav:user:profile;consteldocument.querySelector(#${CSS.escape(id)});// class里带点号也一样比如classcol-md-6// 不能直接写.querySelector(.col-md-
因为.在CSS选择器里表示类选择器// 得写成.querySelector(.col-md-
不对点号在class名里其实不用转义// 等等让我想想... 其实点号在CSS选择器里就是类选择器的标识// 如果class名本身包含点号比如classversion-
1.
0// 那就要写成.querySelector(.version-1\\.0\\.
constweirdClassdocument.querySelector(.version-1\\.0\\.
;第四个坑大小写敏感问题。
HTML的tag name在querySelector里是不区分大小写的因为HTML本身不区分但是XML或者XHTML里就可能区分。
而且属性选择器是区分大小写的这点有时候会让人意外。
比如[data-role“Admin”]和[data-role“admin”]在querySelector里是两个不同的东西。
第五个坑空格和选择器格式。
querySelector对空格的敏感度极高多一个少一个都可能结果完全不同。
比如.querySelector(‘.list .item’)是查找class为list的元素内部的所有item而.querySelector(‘.list.item’)注意没有空格是查找同时具有list和item两个class的元素。
差一个空格意思天差地别。
// 查找class为container内部的class为item的元素后代选择器constitemsdocument.querySelectorAll(.container .item);// 查找同时具有container和item两个class的元素与选择器constspecificItemdocument.querySelector(.container.item);// 查找直接子元素子代选择器constdirectChildrendocument.querySelectorAll(.container .item);性能方面有没有雷区说到性能querySelector虽然香但也不能乱用。
因为它每次调用都会遍历DOM树去匹配选择器如果你在一些高频事件里疯狂调用它那画面太美不敢看。
最典型的就是在scroll或者mousemove事件里实时查询。
比如说你想实现一个效果鼠标移动到哪就高亮哪里的元素。
如果你每次都重新querySelectorAll查一遍所有的可高亮元素那页面稍微复杂一点就直接卡成PPT。
// 反面教材这样写性能极差鼠标一动就查询DOMdocument.addEventListener(mousemove,(e){// 每次鼠标移动都重新查询所有元素constallItemsdocument.querySelectorAll(.hoverable);allItems.forEach(item{// 复杂的计算和样式修改...});});正确的做法应该是缓存查询结果。
如果你知道这些元素不会频繁增删那就把它们存到变量里别每次都重新查// 正面教材提前缓存事件处理里只计算位置consthoverablesdocument.querySelectorAll(.hoverable);document.addEventListener(mousemove,(e){// 直接用缓存的列表不再查询DOMhoverables.forEach(item{constrectitem.getBoundingClientRect();// 判断鼠标是否在元素上...});});还有个细节querySelector的选择器解析是从右向左的跟CSS匹配一样。
所以你的选择器越具体匹配越快越宽泛浏览器遍历的节点就越多。
比如说#app .list .item就比单纯的.item快得多因为浏览器可以先通过ID快速定位到app再往下找而不是遍历整个DOM树找所有item。
另外如果你只是简单地通过ID找元素getElementById其实比querySelector(‘#id’)快那么一丢丢。
虽然现代浏览器优化得很好差距微乎其微但是在一些极端性能敏感的场景比如每秒执行几千次的动画这点差距可能会累积起来。
所以别盲目崇拜querySelector该用getElementById的时候还是用它毕竟是原生为ID查找优化的。
骚操作合集这些用法你可能没想到用了这么多年querySelector我发现了一些冷门但超实用的骚操作分享给你们。
第一个状态选择器。
配合表单元素的各种状态伪类可以不用写额外的JS逻辑就能选到特定状态的元素。
比如你想一键获取所有被选中的复选框// 获取所有被选中的复选框一行代码搞定constcheckedBoxesdocument.querySelectorAll(input[typecheckbox]:checked);// 获取所有未选中的constuncheckedBoxesdocument.querySelectorAll(input[typecheckbox]:not(:checked));// 获取所有被禁用的输入框constdisabledInputsdocument.querySelectorAll(input:disabled);// 获取所有非空的输入框用户填了内容的constfilledInputsdocument.querySelectorAll(input:not(:placeholder-shown));第二个排除特定元素。
:not()伪类超级好用可以排除某些特定条件的元素。
比如你想给所有按钮加样式但排除掉disabled的// 选所有button但排除class包含disabled的constactiveButtonsdocument.querySelectorAll(button:not(.disabled));// 或者排除特定类型的constnonSubmitButtonsdocument.querySelectorAll(button:not([typesubmit]));第三个向上查找closest()。
这个虽然不是querySelector的方法但通常跟它配合使用。
有时候你拿到一个元素想找它的某个祖先元素以前得写个while循环一直parentNode往上爬。
现在直接用closest()传入一个选择器它会一级一级往上找直到找到匹配的元素或者到达根节点// 假设你点击了一个span想找它外面的那个class为card的容器document.addEventListener(click,(e){constcarde.target.closest(.card);if(card){// 找到了card容器可以对它操作了card.classList.add(highlight);// 甚至还可以在这个容器里向下查找特定元素consttitlecard.querySelector(.card-title);console.log(点击了卡片:,title.textContent);}});第四个在Shadow DOM里使用。
如果你在用Web ComponentsShadow DOM内部的元素外部是查不到的但是你可以在Shadow Root上调用querySelector// 假设你有个自定义组件它用了shadow DOMconstmyComponentdocument.querySelector(my-component);constshadowRootmyComponent.shadowRoot;// 在shadow root内部查询跟普通document一样用constinternalBtnshadowRoot.querySelector(.internal-button);internalBtn.addEventListener(click,(){console.log(点了shadow DOM里的按钮);});第五个组合选择器玩出花。
你可以用逗号分隔多个选择器一次性查询多种不同类型的元素// 一次性拿到所有的标题和段落constcontentElementsdocument.querySelectorAll(h1, h2, h3, h4, h5, h6, p);// 或者拿特定几个class的元素consttargetsdocument.querySelectorAll(.header, .footer, .sidebar);第六个属性选择器的高级用法。
不只是[attr]这么简单还有[attrvalue]、[attr^value]以value开头、[attr$value]以value结尾、[attr*value]包含value// 找所有src以https开头的图片安全的外部图片constsecureImagesdocument.querySelectorAll(img[src^https]);// 找所有href以.pdf结尾的链接PDF文件constpdfLinksdocument.querySelectorAll(a[href$.pdf]);// 找所有class包含btn-的元素btn-primary, btn-danger等constbuttonsdocument.querySelectorAll([class*btn-]);// 找所有设置了data-id属性的元素不管值是什么constelementsWithIddocument.querySelectorAll([data-id]);遇到查不到元素怎么办排查三板斧最后说说调试技巧。
我相信所有人都遇到过这种情况console.log打印出来是null或者querySelectorAll返回的NodeList长度是0明明元素就在那里啊这时候怎么办我
总结了三板斧百试百灵。
第一板斧检查拼写。
听起来很傻对吧但80%的问题都是因为这个。
特别是大小写JS是大小写敏感的class叫Container和container是两个东西。
还有多打了空格或者把点号写成了逗号这种低级错误我到现在还会犯。
建议直接把选择器复制到浏览器的Elements面板搜索框里试如果搜不到那就是拼写错了。
第二板斧检查时机。
你查询的时机对不对是不是在DOM加载完成之前就查询了或者你是在一个异步操作比如setTimeout、Promise回调里查询但那时候元素还没创建出来/已经被删了在查询之前先console.log一下document.readyState看看状态。
// 调试技巧打印当前状态console.log(当前DOM状态:,document.readyState);console.log(html内容长度:,document.body.innerHTML.length);consteldocument.querySelector(#target);console.log(查询结果:,el);// 如果是null看看上面的信息第三板斧检查作用域。
你是不是在正确的document或者Shadow Root上查询的如果你在一个子iframe里操作却用了parent.document.querySelector那肯定查不到。
或者你获取了一个子树的引用比如const modal document.querySelector(‘#modal’)然后你想查modal里的按钮但却又写了document.querySelector(‘.btn’)而不是modal.querySelector(‘.btn’)。
记住在特定容器内查询要直接用那个容器的引用。
// 错误在全局document里查可能查到其他地方的同名元素constmodalBtndocument.querySelector(.modal-button);// 正确只在modal容器内部查constmodaldocument.querySelector(#loginModal);if(modal){constmodalBtnmodal.querySelector(.modal-button);// 这样即使页面有其他modal也不会混淆}写代码时的小习惯能省大麻烦说了这么多最后分享几个我养成的小习惯能避免很多麻烦。
第一常量管理。
把那些常用的选择器抽成常量别在代码里到处硬编码字符串。
万一产品经理说把user-card改成customer-card你只需要改一处而不是全文替换。
// 定义常量constSELECTORS{USER_CARD:.user-card,DELETE_BTN:.btn-delete,MODAL_CONTAINER:#modalContainer};// 使用constcardsdocument.querySelectorAll(SELECTORS.USER_CARD);cards.forEach(card{constdeleteBtncard.querySelector(SELECTORS.DELETE_BTN);// ...});第二防御性编程。
拿到querySelector的结果先判断是否为空再操作别假设元素一定存在。
页面结构随时可能变后端接口可能返回空数据导致某块DOM不渲染如果不对null做处理代码就会崩。
constbtndocument.querySelector(#submitBtn);// 好习惯先判断再操作if(btn){btn.addEventListener(click,handleSubmit);btn.disabledfalse;}else{console.warn(提交按钮未找到请检查DOM结构);}第三复杂选择器分步查。
如果你有一个超长的选择器比如.document.querySelector(‘.container .wrapper .list-item[data-type“special”]:nth-child(
span’)这种一旦查不到你根本不知道是哪一层出了问题。
不如拆成几步中间加console.log方便调试。
// 复杂查询分步做方便调试constcontainerdocument.querySelector(.container);console.log(container:,container);if(container){constwrappercontainer.querySelector(.wrapper);console.log(wrapper:,wrapper);if(wrapper){consttargetwrapper.querySelector([data-typespecial]);// ...}}第四了解性能边界。
简单的ID查找用getElementById复杂的组合条件用querySelector批量操作缓存结果高频事件避免实时查询。
记住没有银弹只有最适合当前场景的解决方案。
第五善用浏览器DevTools。
在控制台里你可以直接输入$$(‘.item’)它等价于document.querySelectorAll(.item)可以快速测试选择器写得对不对。
还有那个Elements面板的搜索框支持CSS选择器语法是调试选择器的神器。
总之querySelector和querySelectorAll这对兄弟用好了能让你的DOM操作代码简洁优雅用不好也会掉坑里爬不出来。
希望我的这些血泪史能帮你少走点弯路。
记住技术这东西光看文档不够得真刀真枪地写踩过坑才能长记性。
现在就去试试吧祝你写代码少出bug console.log永远打印出你想要的结果