
这一篇已经是系列的第六部分了。前面几篇我们把选择器、DOM操作、事件绑定、动画这些基础过了一遍有读者留言说光看基础不过瘾想看点能直接用到业务里的东西。这篇文章我就换个思路专门挑了一批在真实项目中高频出现的场景来展开——数组操作、动态创建标签、事件冒泡的坑、表单校验还有二维码合成这种稍微综合一点的需求。每个场景我都会给出可直接复用的写法顺手把背后的原理和坑也讲清楚。1. 数组操作判断、添加、去重两行代码解决的事别绕远路先看这个出现频率极高的场景var arr []然后要往里添加数据添加之前还要判断数组里有没有这条数据。很多新手写出来的代码是这样的var arr []; if (arr.length 0) { arr.push(a); } // 添加之前判断是否存在 var flag false; for (var i 0; i arr.length; i) { if (arr[i] a) { flag true; } } if (!flag) { arr.push(a); }功能没问题但在jQuery的环境里这代码显得太啰嗦了。$.inArray()这个方法就是干这个的它返回的是元素在数组中的索引找不到就返回 -1。var arr [a, b, c]; if ($.inArray(b, arr) -1) { arr.push(b); } else { console.log(数组里已经有了); }这里有个关键点判断一定要用 -1因为它返回的是索引位置0也是合法的你要是用if (!$.inArray(...))这种写法索引为 0 的时候会被当成 false直接就是你写了个寂寞。我见过不止一个人在这里踩坑。再进一步如果这个数组不是简单的字符串而是一组对象$.inArray就不太好使了因为它是严格比较对象之间没法直接判断相等。这种场景我一般配合$.grep或者$.each来查var users [ { id: 1, name: 张三 }, { id: 2, name: 李四 } ]; var exists $.grep(users, function(item) { return item.name 张三; }).length 0; if (!exists) { users.push({ id: 3, name: 张三 }); }$.grep的作用是过滤数组传入一个回调函数返回 true 的元素会被保留。过滤完看长度就知道有没有匹配项了。这种方法在写购物车、标签选择器这类功能时特别常用。数组去重是另一个高频需求。jQuery 本身没有专门的去重方法但配合原生的$.unique()可以做一件事——注意$.unique()只对 DOM 元素数组有效对普通字符串数组不起作用。普通数组去重我喜欢直接用 ES6 的Set这跟 jQuery 不冲突混着用很舒服var arr [a, b, a, c, b]; arr $.unique(arr); // 这里对字符串数组没用 // 正确做法 var uniqueArr $.makeArray(new Set(arr));$.makeArray能把类数组对象转成真正的数组配合Set去重两行代码搞定。说到底jQuery 不是万能的它提供的是操作 DOM 和简化事件处理的便利类似去重这种任务原生 ES6 方法往往更直接。2. each遍历你以为会用其实总在this上栽跟头$.each可能是 jQuery 里被用得最频繁的方法之一了。网上关于它的热词里还有jquery foreach说明很多人其实是从其他语言转过来的上来就找类似 foreach 的东西。var data [a, b, c]; $.each(data, function(index, item) { console.log(index item); });这是基本用法。但我发现实际项目中最容易出问题的不是$.each本身而是回调函数里的this。在$.each中this指向当前遍历的元素但注意对于数组来说this就是当前值这个值是原始值类型无法赋值。你可以试试看$.each(data, function(index, item) { this x; // 报错this 不能被赋值 });所以在$.each里要修改数组元素的值直接操作item是没用的你需要通过索引来修改原数组$.each(data, function(index, item) { if (item a) { data[index] aa; } });另一个容易踩的坑是遍历对象。当你$.each一个对象时回调函数里的this指向的是对象的属性值。如果你在回调里调用了一个 jQuery 方法很容易因为this上下文不对报错。我的习惯是在回调函数开头就把需要的值缓存下来避免this带来的迷之问题var config { url: /api, type: POST, timeout: 3000 }; $.each(config, function(key, value) { // 这里的 this value但不要依赖它 console.log(key : value); });再说说跳出循环。JavaScript 原生 forEach 里break是没法用的很多人习惯了之后就以为$.each也一样。其实$.each支持 return false 来终止遍历这算是 jQuery 的一个贴心设计$.each([1, 2, 3, 4, 5], function(index, item) { if (item 3) { return false; // 跳出循环 } console.log(item); // 只会打印 1 2 3 });注意是return false不是return。你只写一个return的话效果等同于 continue只是跳过本次循环。return true也是跳过本次循环。这三个的区别值得记在小本本上写循环逻辑的时候能省不少事。遍历 DOM 元素集合时还有一个常见误区。$(li).each(function() {...})这个用法大家都会但回调里$(this)和$(this).eq(index)是等效的没必要多此一举再构建一次 jQuery 对象直接用$(this)就行。性能上虽然差距不大但代码看起来简洁很多。3. 动态创建标签与内容判断拼接字符串前先想想 XSS热词里有两个问题放在一起看很有意思jquery 创建一个标签和jquery 判断内容中是否有换行符。这俩其实可以发生在同一个场景里——比如你从后端拿了一段文本要动态创建标签展示到页面上先判断文本里有没有换行有换行的就把\n替换成br。先看创建标签的基本姿势var $newDiv $(div, { id: myDiv, class: box, text: 这是一个新创建的标签 }); $(body).append($newDiv);很多新手用$(div)创建标签时会把属性拼进字符串里$(div idmyDiv classbox/div).appendTo(body);两种写法都能用但我更推荐第一种。原因很简单第二种写法要是拼接的属性值里有引号很容易把整个字符串搞崩或者更危险的直接给了 XSS 可乘之机。热词里专门有jquery xss这不是空穴来风。jQuery 早期版本的html()方法对含script标签的字符串处理存在隐患虽然在 3.5.0 之后的版本修复了大部分问题但你自己写代码时也该养成好习惯——能用text()绝不用html()。举个例子如果后端返回的数据里有这样的内容var userContent img srcx onerroralert(1); $(#content).append(userContent); // 危险后端返回的数据你就把人家当纯文本处理用text()方法写入浏览器就老老实实把它当文本展示不会执行里面的脚本var userContent img srcx onerroralert(1); $(div).text(userContent).appendTo(#content); // 页面上显示的是纯文本不会被解析成标签:contains选择器可以用来判断某个元素是否包含指定的文本但注意它匹配的是子字符串不是完整匹配用的时候心里有数就行。再来看换行符的判断。判断一个字符串里有没有换行用正则最方便var content 第一行\n第二行; if (/\r|\n/.test(content)) { console.log(包含换行符); }注意\r也不能漏有些老系统的换行符是\r\n或者单独的\r只匹配\n可能漏掉。把换行转成br展示写法如下var content $(#content).val(); var htmlContent content.replace(/\r\n|\r|\n/g, br); $(#preview).html(htmlContent);这里有个细节如果你用$(#content).text()而不是.val()拿内容取到的是元素的纯文本内容换行符本身是保留的可以正常处理。但如果你通过别的方式拿到了 HTML 内容里面可能已经包含了lt;brgt;之类的转义字符正则匹配的时候就要注意了。项目里如果展示的是用户输入的文本建议先text()获取再替换这样最安全。4. 事件冒泡与“点击某元素消失点击元素内按钮不消失”这是很典型的场景一个弹窗或者下拉菜单你点击它之外的区域时它要消失但点击它内部的按钮时它不应该消失。实现这个功能的核心就在于理解事件冒泡和阻止冒泡。先看一段常见的错误写法$(document).click(function() { $(#popup).hide(); }); $(#popup).click(function() { // 想阻止冒泡但没写对 });如果这个#popup的点击事件里没有阻止冒泡那么点击弹窗时事件会一路冒泡到 document 上document 的 click 处理器就把弹窗隐藏了。表现出来的现象就是你刚点开弹窗想点里面的按钮弹窗就消失了按钮根本点不到。正确的做法是在弹窗的 click 事件里调用event.stopPropagation()$(document).click(function() { $(#popup).hide(); }); $(#popup).click(function(event) { event.stopPropagation(); });这样点击弹窗内部时事件在弹窗这一层就被拦住了不会冒泡到 document 上弹窗自然就不会被隐藏。但这里要注意另一个更隐蔽的坑。如果弹窗内部的某一个按钮它的业务逻辑就是点击后要关闭弹窗这时候直接绑定click事件处理逻辑就行不需要额外调用stopPropagation()因为本来就要消失嘛。真正需要结合用的是这样的场景——弹窗里有个输入框点输入框时弹窗不消失点输入框外的弹窗区域时可以消失$(#popup).click(function(event) { if ($(event.target).is(#inputArea)) { event.stopPropagation(); } });用event.target判断事件来源是个很实用的技巧。event.target指向实际触发事件的元素而this指向绑定事件的元素。一个#popup的点击事件里this永远是#popup但event.target可能是它内部的任意子元素。搞清这两个的区别很多事件的疑难杂症都能迎刃而解。还有一种主动出击的写法不用stopPropagation()而是在 document 的 click 处理里判断点击的目标是否属于某个区域$(document).click(function(event) { var $target $(event.target); if ($target.closest(#popup).length 0) { $(#popup).hide(); } });closest()方法会从当前元素向上查找看它自己或其父级元素里有没有匹配选择器的。如果点击的目标在#popup内部包括它自己closest(#popup)就能找到length 大于 0如果点在外部length 为 0就执行隐藏操作。这种方法有时候比stopPropagation()更好用因为它不干预事件本身的传播适合处理多个弹窗共存的情况。5. 表单校验与二维码合成两个“真·业务场景”解析热词里还有两个跟业务结合比较紧的需求一个是form jqvalidate一个是把二维码移动到底图上然后调整大小保存成图片。这两块放到一起讲因为它们都是典型的“拿 jQuery 接业务”的活。5.1 jquery.validate 快速接入表单校验是前端必备技能jQuery Validate 插件是标配。最基础的用法是这样的form idmyForm input typetext nameusername idusername input typepassword namepassword idpassword button typesubmit提交/button /form$(#myForm).validate({ rules: { username: { required: true, minlength: 2 }, password: { required: true, minlength: 6 } }, messages: { username: { required: 请输入用户名, minlength: 用户名至少2个字符 }, password: { required: 请输入密码, minlength: 密码至少6位 } }, submitHandler: function(form) { // 校验通过后执行在这里用 AJAX 提交 $.ajax({ url: /api/login, method: POST, data: $(form).serialize(), success: function(res) { console.log(res); } }); return false; // 阻止表单默认提交 } });这个插件有几个细节值得注意。rules里的name属性必须和表单元素的name一致用id是不行的这是一个新手必踩的坑。校验不通过时错误消息默认插入在校验元素后面如果有自定义样式需求可以通过errorElement和errorClass调整。submitHandler我强烈建议写进每个校验里。默认情况下表单校验通过后会走原生的表单提交页面就刷新了这在很多单页应用里是灾难。在submitHandler里拦截并用 AJAX 提交是更合理的方案提交完再决定是跳转还是留在当前页。还有一点如果表单里的有些字段不想校验但在表单提交时又需要它们的值可以给这些字段加上ignore配置$(#myForm).validate({ ignore: .skip-validate, // ... });远程校验比如用户名是否已被注册也是常用的功能用remote规则指向一个后端接口就行接口返回 true 表示校验通过返回 false 或字符串就表示不通过字符串会被当作错误消息展示。注意 remote 校验默认是异步的而且只有当前字段的值发生变化时才会重新请求值没变就提交表单的话remote 校验不会第二次触发。5.2 二维码合成图片的完整思路这个需求背景很常见生成一张带二维码的图片比如海报、工牌或者宣传图。二维码可以是从后端接口返回的图片地址也可以是自己生成的。用户需要把二维码移动到背景图上、调整大小、最终保存成一张新图片。实现方案需要分两半来看第一步是合成第二步是保存。合成的基础是 HTML5 CanvasjQuery 在这里的角色是简化 DOM 操作和事件绑定真正画图的部分得靠 Canvas API。先画背景图再画二维码var canvas document.getElementById(canvas); var ctx canvas.getContext(2d); var bgImage new Image(); var qrImage new Image(); bgImage.onload function() { canvas.width bgImage.width; canvas.height bgImage.height; ctx.drawImage(bgImage, 0, 0); qrImage.onload function() { ctx.drawImage(qrImage, 100, 100, 160, 160); // 绘制二维码 }; qrImage.src https://example.com/qrcode.png; }; bgImage.src https://example.com/bg.png;这里有一个注意点跨域图片直接绘制到 canvas 上会导致 canvas 被“污染”之后再调用canvas.toDataURL()就会报错。解决方式是在后端设置允许跨域的响应头并且在 image 对象上设置crossOrigin属性qrImage.crossOrigin anonymous;移动二维码在 canvas 上的位置本质上是改变绘制时的坐标。你可以监听鼠标的mousedown、mousemove、mouseup事件记录鼠标偏移量然后重新绘制整个 canvas。每动一下都重绘一次性能上是够用的因为 canvas 的绘制效率很高。调整大小可以拖拽右下角的一个手柄根据拖拽距离改变二维码的绘制宽高。计算方式很简单记录初始宽高和拖拽起始坐标然后用鼠标当前位置和起始坐标的差值加上初始宽高就是新的大小。拖拽手柄的核心逻辑var isDragging false; var startX, startY; var qrWidth 160, qrHeight 160; var qrX 100, qrY 100; $(#resizeHandle).on(mousedown, function(e) { isDragging true; startX e.clientX; startY e.clientY; }); $(document).on(mousemove, function(e) { if (!isDragging) return; var dx e.clientX - startX; var dy e.clientY - startY; qrWidth Math.max(50, qrWidth dx); qrHeight qrHeight * (qrWidth / (qrWidth - dx (qrWidth - dx 0 ? 1 : 0))); // 这里简化处理实际保持等比可以直接用 qrWidth * (qrHeight / qrWidth) redrawCanvas(); startX e.clientX; startY e.clientY; }); $(document).on(mouseup, function() { isDragging false; });等比缩放的一个简化写法是固定一个基准比例height width * (originHeight / originWidth)这样只拖一个轴另一个轴自动跟着变化四周不会变形。最终保存时canvas.toDataURL(image/png)拿到 base64 的数据构造一个a标签模拟下载var link document.createElement(a); link.download poster.png; link.href canvas.toDataURL(image/png); link.click();注意如果是跨域图片前面说的crossOrigin必须设置对不然这一步会直接抛SecurityError。这个报错很经典排查方向就是图片能不能跨域加载。6. 别让 jQuery 变成你的安全隐患XSS 与内容安全jquery xss登上热搜词说明越来越多的人开始关注这个问题了。jQuery 的核心优势是操作 DOM 方便但方便的代价是——如果操作不当很容易不小心把用户输入的恶意内容当成了 HTML 插入页面。XSS跨站脚本攻击的本质是程序把用户可控的内容当代码执行了。在 jQuery 语境下最常见的入口就是html()、append()、prepend()、after()、before()这些方法。它们都会把传入的字符串当作 HTML 来解析。之前说过能用text()就用text()。有些场景确实需要插入富文本那入库之前就该做好服务端过滤。前端能做的是至少保证文本内容不用html()传入// 不安全 $(#box).html(userInput); // 相对安全 $(#box).text(userInput); // 需要展示富文本时先自己转义特殊字符 function escapeHtml(str) { var div document.createElement(div); div.appendChild(document.createTextNode(str)); return div.innerHTML; } $(#box).html(escapeHtml(userInput));这个escapeHtml小函数很有用它利用textContent和innerHTML的组合把、、、等特殊字符转成 HTML 实体这样即使用户输入了script也只会被当作纯文本展示。还有一个容易被忽略的入口是 URL 属性。如果你允许用户填写链接然后放进href属性里要警惕javascript:协议的链接// 用户可能输入javascript:alert(1) $(#link).attr(href, userInput); // 合理做法限制协议 if (/^https?:\/\//i.test(userInput)) { $(#link).attr(href, userInput); }事件处理属性的绑定也一样onclick...这种字符串形式尽量不要用用 jQuery 的.on(click, fn)来绑定数据就是数据代码就是代码互不混淆。$.ajax 获取 JSON 数据后如果直接拼进 HTML 片段里再用html()渲染同样有风险。稳妥的做法是把渲染逻辑拆出来每条数据都用text()处理后再插入$.ajax({ url: /api/comments, dataType: json, success: function(res) { var $list $(#commentList).empty(); $.each(res.data, function(i, item) { var $li $(li); $(span).text(item.username).appendTo($li); $(p).text(item.content).appendTo($li); $list.append($li); }); } });再强调一次jQuery 3.5.0 之后的版本已经修复了html()方法里script标签的自动执行问题但这不是你随意拼接 HTML 的理由。习惯决定安全代码习惯才是第一道防线。7. 这一篇的实操心得几句话打包带走做前端这些年用 jQuery 的时间算起来最长。它被很多人嘲笑过时但它在业务中的实际地位尤其是在老项目维护和快速原型开发中依然是不可替代的。把这一篇的几个要点总结一下都是我在实际开发中验证过的经验。数组操作这一块$.inArray判断存在性、$.grep处理对象数组过滤配合 ES6 的Set做去重基本覆盖了业务里九成场景。别再手写 for 循环来查数组了代码是给人读的语义化方法名比注释管用。$.each最重要的认知是return false跳出、return跳过以及回调里的this不能修改原数组的值。这三个点踩过一次坑你就长记性了。每次在$.each里改数据失败的时候回来查一眼这一节能省半小时。创建标签别拼大字符串属性多的时候用$(div, {...})的写法可读性和安全性都高一个档次。凡是往页面里插入用户数据问自己一个问题这里用text()行不行能用就用不能说明你真需要富文本那就得把转义做完整。事件冒泡的“点击外部消失、点击内部不消失”本质是stopPropagation()和event.target的灵活运用。用closest()判断目标归属比层层阻止冒泡来说更符合直觉。jquery.validate 是成熟插件但两件事必须自己拿主意一是submitHandler里写 AJAX 提交二是name属性必须和规则对应。这两点做不到插件用得越顺上线后的坑越深。二维码合成是 Canvas 的活jQuery 负责事件和 DOM 控制。核心记住两点跨域图片要设置crossOrigin保存用toDataURL。图片被画到 canvas 上之后后续所有操作都是重绘。最后是安全问题。XSS 的坑大多是写代码时图省事埋下的别把用户输入当 HTML别用字符串拼事件属性该转义的时候老老实实转义。这些习惯养成了不管以后你用 jQuery、Vue 还是 React都能少交很多安全学费。这系列文档写到这里基础篇差不多完整了。下一篇我会往里填一些动画与异步控制的进阶内容比如用 Deferred 对象管理多个 AJAX 请求的依赖关系这是个容易被忽略但很实战的主题。