1. 破局先看懂三个角色$、DOM 对象、jQuery 对象很多刚接触 jQuery 的人第一周基本都在跟三个名字较劲$、DOM 对象、jQuery 对象。别小看这三个概念我见过不少工作一两年前端的朋友写代码的时候还是靠“试”——this拿不到值就加个$()包一层不行再换[0]试一下代码里经常蹦出$(this)[0].value这种混搭写法。能用但心里其实没底。这篇文章就把这三个角色彻底掰开揉碎讲清楚。尤其下面这条主线是你今天看完必须带走的东西jQuery 对象是“包装后的 DOM 对象”数组式的结构、封装了方法DOM 对象是浏览器原生返回的节点$是 jQuery 的顶级工厂函数负责创建 jQuery 对象。两者需要互相转换本质是因为原生属性和 jQuery 方法不能跨对象调用。这次咱们不讲空理论直接结合我实际调试过的代码场景来拆。学完之后你再看网上的 jQuery 代码不会再有一句是“玄学”。1.1 为什么 jQuery 引入顶级对象 $它到底做了什么$是 jQuery 的顶级对象本质是jQuery这个函数/对象的别名。在 jQuery 源码里最后一行就有类似这样的定义window.jQuery window.$ jQuery;所以你在页面里写$(...)实际上就是调用了jQuery(...)这个函数。它干的最核心的一件事是把你传进去的东西选择器字符串、DOM 节点、HTML 字符串、函数等统一包装成一个 jQuery 对象。我自己早期犯过一个低级错误以为$就是一个“选择器函数”只能写$(#id)这种东西。后来读源码才发现它接收的参数类型至少有四类每类行为还不同传参类型例子返回值说明CSS 选择器字符串$(#app)包装匹配到的所有 DOM 为 jQuery 对象DOM 对象$(document.body)包装单个 DOM 节点为 jQuery 对象HTML 字符串$(div classbox)动态创建元素返回 jQuery 对象函数$(function(){})相当于 DOMContentLoaded 后执行返回空 jQuery 对象这也是“顶级对象”的含义它是整个 jQuery 库的门面你所有操作的入口都经过它。你在很多老项目中看到jQuery.each()、jQuery.noConflict()这类静态方法也是挂在$/jQuery这个顶级对象上的。如果你写过原生 JS你会发现$和document.querySelectorAll有些像但差别很大。querySelectorAll返回的是NodeList一次只能调用有限的属性方法$返回的是一个“类数组”但身上额外挂着css、attr、on、addClass等一堆 jQuery 方法这些方法内部帮你处理了浏览器兼容、遍历、链式调用等事情。1.2 一次搞懂 DOM 对象浏览器的亲儿子DOM 对象就是浏览器在解析 HTML 时生成的节点对象。比如const div document.getElementById(box); const divs document.querySelectorAll(.item);这里的div就是一个HTMLDivElement类型的对象divs是一个NodeList。DOM 对象身上的属性和方法由浏览器原生提供是 W3C 标准定义的。拿div举例你可以直接读它的id、className、innerHTML可以直接改它的style.color也可以手动调用appendChild来插入子节点。这些操作不依赖任何框架是所有前端操作的底层基础。我个人的理解方式是**DOM 对象是浏览器给你的“原料”jQuery 对象是工厂加工后的“半成品”两者都指向页面上的同一个真实元素。**你操作的是同一棵树只是操作接口不一样。很多新手搞不清一个关键点DOM 对象和 jQuery 对象在内存中不是同一个“壳”但指向同一个“核”。你用document.getElementById拿到的对象和你用$(#box)拿到的对象指向的都是那个div idbox只是$(#box)外面多包了一层 jQuery 的方法供你调用。1.3 jQuery 对象的真面目其实是“伪数组 方法集合”我给新手讲 jQuery 对象时最常用的类比是“一个装着 DOM 元素的伪数组且附带工具箱”。const $box $(#box); console.log($box); // 控制台展开会看到0: div#box, length: 1, 以及一堆方法$box本质是一个对象它的数字下标0、1... 存着对应的 DOM 节点length属性记录个数同时它原型上挂着一堆便捷方法。注意这里不能直接调用数组的map、forEach这类原生数组方法因为它不是真正的数组只是长得像。jQuery 对象核心特性我总结了三个词批量、链式、封装。// 批量一行选择器搞定所有同类元素 $(.item).css(color, red); // 链式方法返回自己一路点下去 $(#box).addClass(active).attr(data-id, 1).css(width, 100px); // 封装内部处理兼容和遍历 $(.item).on(click, function() { console.log($(this).index()); });你用一个$()包装后几乎不用自己写for循环遍历多个元素也不用管不同浏览器 API 的差异。这也是 jQuery 当年横扫江湖的核心原因。2. 为什么要互相转换不能混用的痛点这部分可能是全文最重要的内容。搞清楚了“为什么不能混用”你才能真正理解转换的意义而不是死记硬背语法。2.1 直接混用会引发哪些典型报错我在技术群里看到最多的求助帖就是这种$(#btn).on(click, function() { this.style.display none; // 报错不报 });这里this是 DOM 对象调用style是没问题的。但如果写成$(#btn).on(click, function() { this.addClass(hidden); // 报错this.addClass is not a function });就会报错因为this是原生 DOM 元素身上没有addClass这个方法那是 jQuery 对象的方法。反过来的错也常见const box document.getElementById(box); box.css(color, red); // 报错box.css is not a function有人说“那我干脆全都用 jQuery 不就行了”——不行。有些场景你必须拿到原生 DOM 对象比如使用HTMLVideoElement的play()、pause()方法使用canvas的getContext(2d)拿到input的files文件列表某些第三方库明确要求传 DOM 节点而不是 jQuery 对象还有另一种反向需求有些时候你从某个库或框架里拿到的就是原生节点但你想用它调用 jQuery 的快捷方法比如快速fadeOut一下那也得转换。2.2 总结一张表什么场景用谁、什么时候必须转场景推荐用谁说明高频操作样式、类名、事件绑定jQuery 对象链式调用爽代码量少读取表单文件、调用视频播放DOM 对象原生 API 才有这些能力遍历多个元素并做复杂逻辑优先 jQuery 对象.each()比 for 循环省心传给第三方 JS 库视库而定很多库要原生 DOM需要转出去在 jQuery 事件回调里拿当前元素$(this)转成 jQuery 才能用.css()等在原生事件回调里拿当前元素this/event.target原生 DOM 直接用一句话总结在哪个“世界”里做事就用哪个世界的接口。跨世界调用前先转换。2.3 转换是“花架子”吗性能考量说清楚有人会担心每次$()包一层是不是很耗性能是不是应该尽量少转换我的建议是业务代码里不用过分纠结这个性能。jQuery 的$()包装并没有你想象中那么重它只是创建一个对象、把 DOM 节点塞进去、挂上原型方法几百上千次调用都没问题。真正该优化的是减少不必要的 DOM 查询比如在循环里反复$(#app)找同一个元素。但也有一些细节值得注意。比如高频事件里// 不推荐的极端写法每次点击都重新包装 $(#btn).on(mousemove, function() { $(this).css(...); }); // 推荐事件回调里用 $(this) 是常规操作没问题 // 真正要注意的是不要把 jQuery 对象的创建放在超高频率的循环里这里我补充一个我自己实践过的原则事件回调里使用$(this)是常规操作放心用。但如果你有一个 10000 次的 for 循环循环体内别写$(#box)这种查询先把元素存到循环外。3. 核心实操jQuery 对象与 DOM 对象的互相转换讲完概念和痛点下面进入实操环节。这部分我会用最典型的代码片段带你把两种转换方法彻底练熟。3.1 jQuery 对象转 DOM 对象两种方法一个核心思想核心思想非常简单**jQuery 对象是伪数组你只要把里面的元素“取出来”就成了 DOM 对象。**取出来有两条路。第一种下标法最简单直接。const $box $(#box); const box $box[0]; // 取出第一个 DOM 元素 console.log(box.id); // box console.log(box.className); // 可用原生属性 box.style.color red; // 可用原生样式操作如果 jQuery 对象里包了多个元素const $items $(.item); const firstItem $items[0]; // 第一个原生 DOM const secondItem $items[1]; // 第二个原生 DOM第二种.get()方法更语义化。const $items $(.item); const firstItem $items.get(0); // 获取指定索引的原生 DOM const allItems $items.get(); // 不传参数返回所有 DOM 组成的真实数组.get()不传参数时返回的是一个标准数组这时候你就可以用数组的map、filter、forEach了非常实用。我经常这样用$(.item).get().forEach(function(el) { el.style.display block; });两种方法选哪个我的习惯是**取单个元素用下标或者.get(0)都行想遍历所有元素转成数组就用.get()。**如果你需要从 jQuery 对象中取第一个元素还有一种更语义化的.first()但那个返回的还是 jQuery 对象别弄混。3.2 DOM 对象转 jQuery 对象就一招但细节多反过来更简单把 DOM 对象直接传进$()里包一层就行。const box document.getElementById(box); const $box $(box); $box.css(color, blue); $box.addClass(highlight);这里有一个所有新手都会踩的坑用$()包 DOM 对象时字符串别加引号。// 正确传 DOM 变量不加引号 const box document.getElementById(box); const $box $(box); // 错误传字符串 #box 也是对的但那是“选择器”用法不是“包装 DOM”用法 const $box2 $(#box);区别在于$(box)是把已有的 DOM 节点包装成 jQuery 对象$(#box)是让 jQuery 去文档里查找匹配的节点。这两者最终效果在页面只有一个元素时几乎一样但语义不同、性能也不同一个是直接包装一个是走查询流程。还有一种场景事件回调里的this和event.target都是 DOM 对象如果你要在回调里用 jQuery 方法惯例就是包一层$(.item).on(click, function(event) { // this 是 DOM 元素 // event.target 也是 DOM 元素但在事件委托时它可能是子元素 const $current $(this); // 包装当前绑定事件的元素 const $target $(event.target); // 包装真正被点击的元素 $current.addClass(selected); });这里特别注意this和event.target的区别。this指向绑定事件的元素当前 jQuery 对象遍历到的那个event.target指向事件真正发生的元素。如果用事件委托两者可能完全不一样。我在实际项目中就遇到过点击子元素导致event.target不是预期元素、样式加错位置的情况。3.3 完整示例一个模态框打开关闭的转换实战把上面的知识串起来写一个最常见的模态框例子。页面结构div idmodal classmodal div classmodal-content p这是一段提示/p button idcloseBtn关闭/button /div /div button idopenBtn打开模态框/buttonJS 写法// 打开用 jQuery 对象操作 $(#openBtn).on(click, function() { $(#modal).removeClass(hidden); }); // 关闭演示 this 转 jQuery 对象 $(#closeBtn).on(click, function() { // this 是原生 button 元素转成 jQuery 对象再找祖先 const $btn $(this); $btn.closest(.modal).addClass(hidden); }); // 另一种关闭方式演示 jQuery 对象转 DOM 对象 $(#closeBtn).on(click, function() { const modal $(#modal)[0]; // jQuery 对象转 DOM modal.classList.add(hidden); // 用原生 classList });这个例子涵盖了两种转换方向。你看代码也能感觉到jQuery 对象的方法适合“顺着链找、批量改”DOM 对象的方法适合“访问原生属性、调用原生 API”。谁也别瞧不起谁看场景需要。3.4 扩展技巧常见原生 API 配合转换的实战清单下面这份清单是我平时做项目时反复用到的“转换配合点”可以直接抄作业需求转换方式示例代码操作 canvas 绘图jQuery 转 DOM 后调用原生 getContext$(#canvas)[0].getContext(2d)读取文件上传内容jQuery 转 DOM 后读取 files$(#fileInput)[0].files[0]视频播放/暂停jQuery 转 DOM 后调用 play$(#video)[0].play()表单校验原生方法jQuery 转 DOM 后调用 reportValidity$(#form)[0].reportValidity()在 jQuery 回调中使用原生属性DOM 转 jQuery 后调用方法$(this).attr(data-id)传给第三方富文本编辑器初始化DOM 转 jQuery 或反过来看库的要求editor.init($(#editor)[0])获取元素位置做动画两者皆可但 offset() 是 jQuery 的强项$(#box).offset().top有一个特别值得记的点$(this)是在 jQuery 回调中最常见的转换没有之一。因为回调里的this是原生 DOM而你最习惯用的.css、.attr、.closest都是 jQuery 的方法每次都要包一下。这不算性能浪费而是 jQuery 设计上就是这么用的。4. 避坑指南与高频问题排查实录这部分我把这几年带新人时反复遇到的高频问题整理成一份速查表每一行都是血泪教训。4.1 高频报错速查这次你一定不慌了报错信息原因解决方案$(...).addClass is not a function变量不是 jQuery 对象检查是否$()包了一层可能变量是原生 DOMel.css is not a function变量是原生 DOM但调了 jQuery 方法先用$(el)包一层再调用this.getAttribute is not a function回调里this被绑定成了 jQuery 对象需要原生 DOM 时用this[0]或this.get(0)Cannot read property value of undefinedjQuery 对象为空对象$box[0]为 undefined先判断$box.length是否为 0$(...).each is not a function调用的对象不是 jQuery 对象而是数组对 jQuery 对象用.each()对数组用$.each()或原生 forEach看到第一个和第二个的区别了吗一个是被包装过度一个是包装缺失。很多新手遇到报错就四处乱改其实只要想清楚“当前变量是哪种对象”问题就解决了一半。4.2 常见问题为什么$(this)和this结果不一样这是一个高频困惑点。比如下面的代码$(.item).on(click, function() { console.log(this); // DOM 元素 console.log($(this)); // jQuery 对象 console.log(this $(this)[0]); // true });this和$(this)[0]是同一个东西。$(this)只是给它套了一层 jQuery 的壳。控制台打印出来不一样是因为一个是原生对象结构、一个是 jQuery 对象结构但指向的 DOM 节点完全一致。想验证很简单在两个变量上都改一下style.color页面元素的颜色都会变因为它们操作的是同一个真实节点。这点理解了你就不会在this和$(this)之间反复横跳了。4.3 从热搜问题聊起获取第一个子元素到底怎么写最近很多人搜“jquery 第一个子元素”我顺带把这个高频需求讲透。这背后其实还是对象选择的问题。// 方法一CSS 选择器返回 jQuery 对象 $(.parent :first-child); $(.parent).children().first(); $(.parent).find(*).first(); // 方法二eq(0)返回 jQuery 对象 $(.parent).children().eq(0); // 方法三拿到 DOM 对象 $(.parent)[0].firstElementChild;如果你只想拿第一个子元素并操作它的样式用.children().first()最简单如果你要做条件判断比如“第一个元素是否有某个类”很多人会顺手用.first()然后一路链下去。注意.first()返回的仍是 jQuery 对象别在这上面调原生方法要转就加[0]。我处理这类需求时更习惯先想清楚“我要的是 DOM 还是 jQuery”而不是“哪个方法看起来短”。上面四种写法我全都在项目里用过没有绝对的优劣只有适不适合当前上下文。4.4 新手最容易踩的三个隐形坑第一个隐性坑在循环中反复创建 jQuery 对象。// 不推荐 for (let i 0; i 100; i) { $(#box).css(width, i px); } // 推荐把查询结果存下来 const $box $(#box); for (let i 0; i 100; i) { $box.css(width, i px); }这个优化看似很小但在复杂页面、高频交互场景下减少重复查询对流畅度的提升非常明显。如果你用 jQuery 写表格排序、拖拽、缩放这些频繁操作这一条尤其有用。第二个隐性坑用 jQuery 对象判断是否存在时踩空。// 错误习惯 if ($(#box)) { // 永远为 true因为 jQuery 对象即使为空也是真值 } // 正确姿势 if ($(#box).length) { // length 为 0 表示没有匹配元素 }这个是新手入门时最经典的一个坑。$(#notExist)不会返回null而是返回一个length为 0 的 jQuery 对象。用if判断永远是真所以要判断元素是否存在于页面上必须看length。第三个隐性坑用$(this)时不小心包了两次。$(.item).on(click, function() { const $this $(this); // 如果你在这里再写 $($this)就多包了一层 // $($this) 虽然不会报错但会产生一个嵌套结构下标访问会奇怪 });很多人写顺手了会在函数内部写$($(this))这虽然不报错但拿到的对象顶层下标就变了后续$wrapper[0]取到的可能是之前的 jQuery 对象而不是 DOM 元素排查起来特别费劲。我的建议是要么就老老实实const $this $(this)要么就用原生this别反复包。4.5 再聊热搜里的“jquery 还有必要学吗”我的真实看法最近这个讨论挺多的我写这篇转换教程的时候也认真想过这个问题。我的观点很明确**如果你只做全新项目且团队已经全面使用 Vue 或 React那 jQuery 的新项目应用场景确实在变少但如果你要维护老项目、看历史代码、或者做一些轻量营销页jQuery 的理解完全没过期。**它的核心思想——选择器、链式调用、对象包装——深刻影响了现代前端框架的 API 设计。你学 jQuery 的过程中积累的“对象模型思维”在 Vue 的 ref、React 的 useState 里都能找到影子。往实际一点说现在仍有大量 CMS 后台、活动页、企业内部工具在用 jQuery你如果接到这类维护需求看懂 $ 对象和 DOM 对象的关系比背 API 重要得多。这也是我坚持把这篇基础内容写透的原因。5. 实际项目中的应用模式与进阶思考基础语法讲完最后聊聊实际项目里这些转换知识的应用模式。这部分是给已经能写 jQuery 但想提升代码质量的你。5.1 从架构角度理解为什么 jQuery 对象方法不能直接挂原生原型有人问过一个问题为什么 jQuery 不把方法直接挂在 DOM 对象的原型上这样不就不用转换了答案其实很简单**如果扩展原生对象的原型会污染全局 —— 不同库、不同版本的代码都可能覆盖同一个方法而且浏览器的原生对象升级后可能产生冲突。**jQuery 选择“包装对象”的模式是一种经典的“组合优于继承”设计。它通过闭包保存原生节点引用通过原型方法提供增强能力对外只暴露一个$入口。你现在看 Vue 的虚拟 DOM、React 的合成事件某种程度上都延续了这种“包装 委托”的思想。理解了这层你再看$(#box)就不会觉得它只是个“找元素”的函数了。它是一个“对象增强器”——把原生节点包成功能更强大的 jQuery 对象。$(document).ready()能绑定文档就绪事件、$(window).scroll()能绑定窗口滚动也都是因为$()包装了这些原生全局对象。5.2 项目中的经典组合jQuery 对象做逻辑DOM 对象做边界我写项目时慢慢形成了一个习惯被团队称为“双世界协作法”分享给你jQuery 对象负责“泛化操作”查找、遍历、绑定事件、批量改样式、动画DOM 对象负责“精准调用”读取原生 API、对接第三方库、访问复杂属性举个例子做一个拖拽排序功能const $items $(.sortable-item); $items.on(mousedown, function(e) { const domItem this; // 原生 DOM const $item $(this); // jQuery 对象 // 用原生 DOM 获取坐标等边界信息 const startX e.clientX; const rect domItem.getBoundingClientRect(); // 用 jQuery 对象做样式反馈 $item.addClass(dragging).css({ position: fixed, left: rect.left px, top: rect.top px }); });在这个例子里getBoundingClientRect是原生方法只能在 DOM 对象上调用而.addClass、.css是 jQuery 方法只能用 jQuery 对象调。你不需要纠结哪种方式更高级真实项目就是这样“混着来”关键是逻辑清晰、边界分明。5.3 一个容易被忽略的细节$(document).ready的典型使用与转换无关的陷阱很多人知道$(function(){})是“文档就绪后执行”但不知道它和window.onload的区别。window.onload要等所有图片、样式、外部资源都加载完成$(document).ready只要 DOM 结构解析完就执行。如果你在onload里做 DOM 操作用户可能会感受到延迟用$(document).ready则更快。// 三种等价写法 $(document).ready(function() { ... }); $(function() { ... }); jQuery(function($) { ... }); // 内部 $ 参数化使用最后一种写法常常出现在 jQuery 插件开发里它的好处是即使全局$被其他库占用在这个函数内部$依然指向 jQuery 对象。这也是很多老手写插件时的标准防御姿势。5.4 jQuery 与 vue/react 共存时代的转换思维延伸现在不少项目是 Vue/React 为主、jQuery 为辅。这种共存场景下对象转换的思维同样适用甚至更关键。举个例子你在 Vue 里通过ref拿到了一个 DOM 节点// Vue 2 中通过 ref 拿 DOM this.$refs.myDiv; // 原生 DOM // 若想用 jQuery 方法处理这个节点 const $myDiv $(this.$refs.myDiv); $myDiv.slideUp();这里$(...)包原生 DOM 的用法和纯 jQuery 项目完全一样。同理如果你从 jQuery 插件里拿到的对象要传给 Vue 的数据那前提也是先把 jQuery 对象里的 DOM 节点取出来。所以我说对象转换不只是 jQuery 的基础知识更是一种“跨库协作”的底层能力。你今天在这里花一个小时弄懂它以后无论用什么框架遇到“这个库要原生节点、那个库要 jQuery 对象/React 元素”之类的对接需求脑子里都会有个清晰的转换地图。6. 老手经验与最后的踩坑心得既然文章走到这里我再掏点实际经验出来。这些细节可能不会出现在官方文档里但对你的开发体验影响非常大。6.1 我在真实项目中积累的 5 条铁律第一条铁律拿到不确定类型的变量时先打印再动手。我排查别人代码时经常看到“一行一行试”的调试方式。最高效的方式是console.log(varName)然后看控制台里它是不是 jQuery 对象。如果是展开后能看到0: div#id和length如果不是就是一个普通对象。判断完再写后续代码少走无数弯路。第二条铁律事件回调里的this与event.target要区分特别是在事件委托中。委托时this是委托者比如$(#list)event.target是真实点击的子元素。我见过一个 bug就是因为有人误以为两者一样导致点击空白区域时event.target变成了ul本身样式加在了错误的节点上。想要稳定的“当前点击项”最好都用 jQuery 的.closest()来定位。第三条铁律在封装函数时入参和出参要统一要么都收 jQuery 对象、要么都收 DOM 对象。我的个人习惯是内部工具函数接收 jQuery 对象返回 jQuery 对象对接第三方库时才额外提供 DOM 对象的入口。这样内部逻辑不会在两种对象之间反复横跳代码可读性高很多。第四条铁律善用.get()转成数组再配合原生数组方法能让代码简洁不少。比如要从选中的列表项中筛选出包含某个类名的项用 jQuery 的.filter()也能做但当你需要更复杂的组合处理时$items.get().filter(...)反而更顺手。第五条铁律时刻关注length。不管是判断元素是否存在、循环是否要执行、还是检测动态加载内容是否成功length都是最重要的判断依据。你可以在开发初期养成习惯每个可能为空的选择器后面先想想length是否为 0。6.2 再补一个关于“jquery datatable 单元格内容过长”的经验最近看到有人在搜“jquery datatable 单元格内容过长展示、鼠标悬浮展示全部数据”这其实也涉及 DOM 对象和 jQuery 对象的配合。简单分享一下我的实现思路渲染表格时给需要展示完整内容的单元格加一个自定义属性比如>// 1. 打印对象本身 console.log($(#box)); // 2. 检查原型链上的构造函数名 console.log($(#box).constructor.name); // jQuery console.log(document.getElementById(box).constructor.name); // HTMLDivElement // 3. 判断是否为 jQuery 对象 function isJQuery(obj) { return obj instanceof jQuery; } // 4. 判断是否为 DOM 对象常规做法 function isDOM(obj) { return obj instanceof Element || obj document; }这几个小函数我经常放在调试代码的顶部遇到不确定的变量就套一下。熟练之后你在开发工具里一眼就能看出当前变量是 DOM 对象还是 jQuery 对象再用对应的方法基本不会再报“xxx is not a function”的错了。
