jQuery对象与DOM对象互转:本质差异与实战避坑指南

📅 发布时间:2026/9/25 8:03:54
jQuery对象与DOM对象互转:本质差异与实战避坑指南
写 jQuery 写了两三年见过不少新同事第一个卡壳的地方不是复杂插件反而是最基础的三个概念$到底是什么、document.getElementById拿到的对象和$(#id)拿到的对象差在哪、为什么有时候能直接.val()有时候又要[0]一下。这套对象体系不搞明白写出来的代码经常是“一半能跑一半报错”排错都要多花一倍时间。今天就把这套东西彻底理干净先说$顶级对象再说 DOM 对象和 jQuery 对象的本质差异最后把互相转换的几种写法和典型坑一次性讲透。内容偏向实战适合刚接触 jQuery 的新人也适合平时写原生 JS、偶尔要维护老项目的人看完可以直接照着抄。1. 先把三个角色认清楚$、DOM 对象、jQuery 对象1.1 顶级对象 $ 到底是什么你可以在浏览器控制台里直接敲一行代码验证一下console.log(window.jQuery window.$); // true console.log(typeof $); // function$不是独立的新语法也不是什么神秘符号它就是 jQuery 库向外暴露的全局函数和jQuery完全等价。你平时写的$(#id)本质是调用了jQuery(#id)两者没有任何区别。可以这样理解$是 jQuery 所有功能的“总入口”。不管你是要选元素、遍历集合、绑定事件还是调用$.ajax、$.each这类工具方法都要通过这个入口进去。它同时具备两种身份作为函数调用比如$(div)负责把选择器或 DOM 节点包装成 jQuery 对象作为对象挂载属性比如$.ajax、$.trim、$.fn这些是 jQuery 直接暴露的静态方法。用个生活化类比$就是一个服务窗口你递上什么材料选择器字符串、DOM 对象、函数、HTML 片段它给你开出对应的“回执jQuery 对象”。窗口本身还挂着各种办事指南静态工具方法。很多初学者误以为$只能用来选元素这是最常见的误解。其实$(document).ready是在监听 DOM 就绪$(function(){})是它的简写$(div)是动态创建节点$(window).scroll是给原生对象直接挂事件。这些都是$作为函数入口的体现。1.2 DOM 对象与 jQuery 对象的本质差异DOM 对象是浏览器原生的东西。你用document.getElementById、document.querySelector、element.childNodes这些原生 API 拿到的就是 DOM 对象。它可以直接操作innerHTML、style、value、textContent这些属性也可以用addEventListener绑定事件。jQuery 对象则是$()把 DOM 对象“包装”之后得到的增强型对象。它内部其实是一个类数组结构把匹配到的若干 DOM 节点按顺序塞进数字索引里同时挂上length属性再把 jQuery 原型链上的一大堆方法.css、.on、.hide、.addClass这些都暴露给你用。两者的核心差异用一张表就能说清对比项DOM 对象jQuery 对象获取方式document.getElementById、querySelector等原生 API$(选择器)、$(DOM对象)本质类型原生对象可能是HTMLElement、Text节点等Object类型的类数组对象方法体系只能调用 DOM 原生方法只能调用 jQuery 方法链式调用通常不支持支持一行代码连续操作批量操作需要自己循环内部自动遍历匹配到的所有节点是否带 length单个元素没有NodeList有有且带数字索引一个常见误用就是混着调方法。看过不少代码在写$(#id).value结果拿到的是undefined也有人写document.getElementById(id).val()直接报错。原因就是两种对象的方法体系完全不同。再补一个容易忽略的点jQuery 对象长得像数组但它不是Array实例。你不能直接在它身上用forEach、push、pop这些原生数组方法它内部实现了一套自己的迭代方法.each、.map等。知道这一点之后你就能理解为什么很多人转数组时要用.toArray()或者Array.prototype.slice.call()。2. 拆开 $ 的包装从调用过程看对象生成2.1 $() 内部是如何“new”出来的很多教程会告诉你“$()是一个构造函数”这话不准确但也不是完全错。jQuery 源码里有一段极简又很关键的逻辑类似下面这样jQuery function(selector, context) { return new jQuery.fn.init(selector, context); };也就是说你调用$(li)的时候内部实际执行的是new jQuery.fn.init(selector, context)。关键在于后面这几行源码的精妙配合jQuery.fn jQuery.prototype; jQuery.fn.init.prototype jQuery.fn;jQuery.fn就是jQuery.prototype而init.prototype又指回jQuery.fn。这个循环引用让new init()创建出来的对象虽然构造函数是init但它能沿着原型链访问到jQuery.prototype上挂载的所有方法。这也是为什么你能写出$(li).css(color, red).addClass(active)这种连续调用——因为每一次方法执行完返回的仍然是 jQuery 对象链式调用才能成立。同时$.fn暴露给外部插件作者大家写$.fn.myPlugin function(){}本质上就是在往 jQuery 的原型对象上添加新方法。理解这一步对排查问题很有帮助如果你在控制台打印$(div)看到的是一个看起来像数组、但构造函数不是Array的对象这是正常的不是写错了。2.2 类数组结构带来的便利与限制jQuery 对象内部结构大致是{ 0: div.box, 1: div.item, length: 2, selector: div, context: document, // 原型上的各种方法... }数字索引直接指向原生 DOM 节点这就是为什么$(div)[0]能拿到原生 DOM 对象。便利在于你既能享受 jQuery 方法的便捷又能随时通过索引“钻回”原生世界。限制也同样明显。下面的代码会报错var $lis $(li); $lis.forEach(function(li) { // TypeError: $lis.forEach is not a function // ... });因为你拿到的不是真正的数组数组方法不存在。要遍历 jQuery 对象正确姿势是$lis.each(function(index, element) { // this 指向当前 DOM 元素 });或者先转成数组再用原生方法$lis.toArray().forEach(function(li) { // li 是 DOM 元素 });想判断一个变量到底是不是 jQuery 对象可以用变量 instanceof jQuery更靠谱的写法是检查它的jquery属性jQuery 内部会为每个对象挂一个类似3.7.1的版本字符串if (obj obj.jquery) { // 这是 jQuery 对象 }这些细节平时用不到但一旦遇到“为什么我的对象没有 .each 方法”“为什么 forEach 不可用”这种问题就能快速定位到类型上。3. jQuery 对象与 DOM 对象互相转换的完整操作3.1 DOM 转 jQuery$(原生节点) 的注意事项把 DOM 对象转成 jQuery 对象的写法非常统一就是把原生对象直接丢进$()里var domBox document.getElementById(box); var $box $(domBox); // 也可以直接传 this、事件对象里的 target、window、document $(this).addClass(active); $(e.target).hide(); $(document).on(click, handler); $(window).scroll(function() {});转换的意义在于包装之后就能调用 jQuery 的方法和链式操作比如下面这段代码用原生写法要手动维护 class用 jQuery 就一行// 原生需要自己写字符串拼接和替换逻辑 domBox.className box active; domBox.className domBox.className.replace(active, );// jQuery转换后原地起飞 $(domBox).addClass(active).removeClass(active);注意几个边界情况。$(null)和$(undefined)不会报错返回的是一个空的 jQuery 对象length为 0。这在写通用函数时是件好事你不需要每次做空值判断。别对已经是 jQuery 对象的变量再包一层$()比如$( $box )这几乎没有意义还可能得到预料之外的结构。如果你不确定某个变量是不是已经包装过就用前面说的obj.jquery判断一下再决定要不要转换。3.2 jQuery 转 DOM索引、get()、toArray()往回转同样简单有三种常用方式var $div $(div); // 第一种数组索引 var dom1 $div[0]; // 第二种get() 方法等价于索引 var dom2 $div.get(0); // 第三种toArray() 转成真正的数组 var domArray $div.toArray();get()有个小绝活支持负数索引$div.get(-1)拿到的就是集合里最后一个元素原生数组的arr[arr.length - 1]写起来繁琐多了。var $items $(.item); var first $items.get(0); var last $items.get(-1);toArray()返回的是真正数组拿到之后可以放心用forEach、filter、map这些原生方法适合和现代 JS 风格混写$(li.visible).toArray().filter(function(li) { return li.innerText.indexOf(关键词) ! -1; }).forEach(function(li) { li.style.color red; });还有一点要注意jQuery 对象里如果没有任何匹配项$div[0]的结果是undefined。取索引时需要先判断一下不然容易在后续访问dom.innerHTML时炸出Cannot read properties of undefined。3.3 转换前后行为差异对照转来转去容易糊涂我把常见操作在两种对象下的写法对照放在一起方便你照着写操作DOM 对象写法jQuery 对象写法读取文本dom.textContent$obj.text()设置文本dom.textContent 你好$obj.text(你好)读取 HTMLdom.innerHTML$obj.html()设置 HTMLdom.innerHTML b加粗/b$obj.html(b加粗/b)读取表单值dom.value$obj.val()设置表单值dom.value abc$obj.val(abc)设置样式dom.style.color red$obj.css(color, red)添加类dom.classList.add(active)$obj.addClass(active)删除类dom.classList.remove(active)$obj.removeClass(active)绑定事件dom.addEventListener(click, fn)$obj.on(click, fn)解绑事件dom.removeEventListener(click, fn)$obj.off(click, fn)看一个混合使用的完整示例你会更直观地感受到为什么要转换var $btn $(#submit); $btn.on(click, function() { // 回调里 this 是原生 DOM 按钮 $(this).prop(disabled, true); // 先转 jQuery再调用 prop var inputDom document.getElementById(username); // 原生获取 var value $(inputDom).val(); // DOM 转 jQuery 后取值 if (!value.trim()) { alert(用户名不能为空); $(this).prop(disabled, false); } });这个场景很典型事件回调里拿到的this是 DOM 对象你需要$(this)转成 jQuery 对象才能舒服地链式操作。而页面里有原生 JS 代码时你又要用$(inputDom)把原生对象包装起来让代码风格统一。4. 真实项目中为什么必须掌握互转4.1 混写原生 JS 与 jQuery 的衔接维护老项目时最常见的情况是页面里既有原生 JS 写的模块又有基于 jQuery 写的组件两者经常要协作。比如一个列表点击高亮功能监听部分用原生写视觉反馈用 jQuery 写document.querySelectorAll(.nav-item).forEach(function(item) { item.addEventListener(click, function() { $(this).addClass(active) // 转换后进入 jQuery 链式 .siblings() .removeClass(active); }); });如果不转换this上压根没有addClass、siblings这些方法只能自己手动写一堆classList操作代码立刻膨胀。反过来如果你在用 jQuery 组件组件内部回调需要把数据传给原生函数也要先取出 DOM 对象再传。还有一种代码洁癖问题有人会在一个项目里一会儿用 jQuery、一会儿用原生 API互相之间还不转换最终结果就是控制台一堆xxx is not a function。掌握互转之后原生与 jQuery 就能无缝衔接你完全可以根据模块需求选择最顺手的一套。4.2 插件回调与第三方控件中的对象陷阱jQuery 生态里很多插件的回调函数会给你一个 DOM 对象而不是 jQuery 对象这是最容易踩的坑。举个例子用 jQuery 的each遍历列表$(li).each(function(index, element) { // 这里的 this 和 element 都是原生 DOM 元素 console.log(this element); // true $(this).css(margin-bottom, 10px); // 必须先转换才能调 css });很多新手拿到element之后直接写element.text(内容)报错后才反应过来。这不是偶然插件的回调设计通常遵循原生优先原则确保低耦合。再看服务端控件场景比如老项目里 ASP.NET 的 GridView 生成出来的元素 ID 经常是ctl00_ContentPlaceHolder1_gvList_btnEdit这种带复杂前缀的名字用$(#ctl00_...)选择特别痛苦。常规做法是给控件加ClientIDModeStatic让 ID 固定或者用属性选择器避开 ID$([id$btnEdit]).on(click, function() { // $ 表示属性以指定字符串结尾这个技巧老开发者都在用 });这些场景都说明一个道理只要你在真实项目里和 jQuery 打交道就得习惯“回调给你什么对象、你需要什么对象、怎么转过去”这个思维流程。4.3 性能视角批量操作时缓存和转换的取舍有人担心每次$()包装都有性能损耗于是干脆否定 jQuery。实际测试下来现代浏览器里创建 jQuery 对象的开销并不大真正影响性能的是频繁操作 DOM 引起的重排重绘。但循环里反复写$(#list li)这种选择器仍然不推荐因为每次都会重新走一遍选择器查找流程。更合理的做法是循环外缓存好 jQuery 对象循环里直接用如果只改原生属性就直接操作 DOM 对象省去中间包装// 不推荐的写法循环里反复查询 for (var i 0; i 100; i) { $(#list li:eq( i )).css(display, none); } // 推荐先生成 jQuery 集合再用索引取原生 DOM 操作 var $allItems $(#list li); var domItems $allItems.toArray(); for (var i 0; i domItems.length; i) { domItems[i].style.display none; }批量处理复杂样式时原生操作更快需要链式组合、事件绑定、动画时jQuery 对象更方便。转换的价值就在这里它不是让你二选一而是让你在两种能力之间自由切换用最合适的工具干最合适的活。5. 常见坑与排查心得5.1 新手最容易碰到的 4 个对象错误我整理了平时答疑时出现频率最高的四类问题每一条都配了正确写法。第一类对 jQuery 对象调用原生属性。$(#username).value; // undefinedjQuery 对象没有 value 属性 // 正确$(#username).val() 或 $(#username)[0].value第二类对 DOM 对象调用 jQuery 方法。document.getElementById(username).val(abc); // TypeError // 正确$(document.getElementById(username)).val(abc)第三类直接用比较两个 jQuery 对象。$(#a) $(#b); // 永远 false因为每次 $() 都创建新对象 // 正确$(#a)[0] $(#b)[0]或用 .is() 比较第四类在each回调里忘了this是 DOM 对象。$(p).each(function() { $(this).css(color, red); // 少了$()会报错 });这四类错误报错信息各不相同但根子都在对象类型的认知上。如果你遇到“明明方法存在却说 not a function”或者“取到的值是 undefined”这类诡异问题第一反应就应该是查一查对象类型。5.2 判断对象类型的几种稳写法写通用组件时经常需要兼容“传入的是 DOM 对象还是 jQuery 对象”。给你几种判断方式function myUtil(elem) { if (!elem) return; if (elem.jquery) { // 已经 jQuery 对象直接用 } else if (elem.nodeType) { // DOM 元素包装一下 elem $(elem); } else if (elem instanceof NodeList) { // 一个节点集合可以转成数组或者直接 $() elem $($.makeArray(elem)); } }这里的关键点是nodeType属性DOM 元素节点都有这个属性元素节点为 1而 jQuery 对象没有。instanceof jQuery虽然也能用但跨框架、多版本环境里偶尔会失灵elem.jquery这种鸭子类型判断更稳。5.3 老话题jQuery 还有必要学吗说实话新项目选型我不会强推 jQuery现在原生querySelector、classList、fetch已经非常强大。但“有没有必要学”要看你处在什么环境老项目维护绕不开许多开源项目和插件源码里全是 jQuery 的写法就算你是新项目理解“框架对象与原生对象之间的转换”这个思维也很有用。Vue 里有ref和原生 DOM 的区分React 里有event.target和合成事件的界限Angular 里也有ElementRef与原生节点的关系。你会发现只要框架封装了 DOM就一定存在“包装对象和原生对象”的转换问题。jQuery 教给你的不是具体的 API 记忆而是一套对象体系的抽象思维这种能力是跨框架迁移的。在这个前提下我仍然建议新同学花时间学一学 jQuery 的对象体系特别是今天讲的这东西。不是为了回到 jQuery 时代而是为了在老代码、新框架之间游刃有余。6. 两个高频场景的小实操6.1 快速获取第一个子元素的几种写法日常开发里“取某个元素的第一个子元素”用得非常频繁。很多同学分不清:first-child、:first和.first()三者区别这里一次说清。:first-child是 CSS 伪类匹配“作为父元素第一个子节点”的元素。你可以直接放进选择器$(ul li:first-child).addClass(highlight);:first是 jQuery 扩展伪类取的是整个匹配集合的第一个$(ul li:first).addClass(highlight); // 相当于 $(ul li).first()区别在哪:first-child可能匹配多个元素比如页面里有一堆ul每个ul的第一个li都会被命中:first永远只返回匹配集合里的第一项。如果已经拿到父元素更推荐用children().first()$(#list).children().first().addClass(active); $(#list).children(:first-child).addClass(active); // 效果相似实际项目中我习惯先看语义要“所有父元素下各自的第一项”用:first-child要“整个集合里的第一个”用.first()或:first。6.2 表格单元格过长时鼠标悬浮展示完整内容这也是网上搜得很多的场景表格列宽固定内容太长被截断鼠标悬停后想看完整数据。最简单的方案是设置title属性但样式不可控只能看系统默认提示而且一点就没了。想更可控就用 jQuery 做个悬浮层。思路如下给要截断的单元格套一个样式.cell-overflow { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }然后 jQuery 绑定hover事件动态生成一个跟随鼠标的提示浮层$(document).on(mouseenter, .cell-overflow, function(e) { var fullText $(this).text().trim(); if (!fullText) return; var $tips $(div classcell-tips/div) .text(fullText) .css({ position: fixed, top: e.pageY 12 px, left: e.pageX 12 px, background: #333, color: #fff, padding: 6px 10px, borderRadius: 4px, fontSize: 12px, zIndex: 9999, maxWidth: 400px }) .appendTo(body); $(this).data(tips, $tips); }).on(mouseleave, .cell-overflow, function() { var $tips $(this).data(tips); if ($tips) { $tips.remove(); $(this).removeData(tips); } });如果你用的是 DataTables 这类插件回调里拿到的td或cell元素经常是 DOM 对象操作前记得先$(cell)包一层这就又回到今天的主角——对象转换上了。整个思路不复杂核心就是“把原生的鼠标行为转成 jQuery 事件再用 jQuery 动态创建节点、定位、销毁”。我在实际项目里验证过这个方案在大数据量表格下也稳定因为用的是事件委托单元格多也不会卡。后来为了更规范我把浮层内容改成了从>