Skip to content

2-21JavaScript实战-放大镜案例 ​

本页对应 第二章WebAPIs,共 7 段视频,约 58 分钟。

学习目标 ​

  • 能独立分析放大镜案例的 small、middle、large 三个盒子结构,并说明四个业务功能分别对应的事件与交互结果。
  • 能在 small 上使用 mouseover 事件委托,判断目标为 img 后读取其 src 同步到 middle 图片,并通过 classList 为当前图片父级添加 active、移除其他同级 active。
  • 能使用 mouseenter、mouseleave 控制 large 与遮罩层的显示隐藏,将中图地址设置为 large 背景图,并使用定时器延迟隐藏且在再次进入时清除定时器。
  • 能在 middle 上监听 mousemove,使用 offsetX、offsetY 设置遮罩层 left、top,减去遮罩宽高一半实现居中,并使用 pointer-events: none 避免遮罩层引起坐标抖动。
  • 能动态计算 maxX、maxY 将遮罩层限制在 middle 范围内,并在鼠标移动时通过 background-position 按 -2 倍 X/Y 反向移动 large 背景图,实现大图跟随效果。

知识点 ​

先看本节笔记 ​

classList.contains 判断类名示意图

本节笔记只有一句核心:

js
元素.classList.contains()

元素.classList.contains() 看看有没有包含某个类,如果有则返回 true,没有则返回 false。

这句话虽然短,但它是后面做“排他思想”“切换状态”“判断当前元素是否已经选中”时非常常用的一个 API。转写稿里的放大镜案例反复操作 active 类:鼠标经过小图片时,给当前小图片的父级 li 添加 active,并移除其他 li 的 active。本节就把“怎么判断有没有类”这件事补完整。

为什么需要判断是否有类 ​

为什么需要: 页面上很多状态是靠类名控制的,比如选中、显示、隐藏、加边框。转写稿里小图片的绿色边框,不是直接改 style,而是给 li 添加 active 类。只要类在,样式就在;类不在,样式就没了。可是在添加或删除之前,我们有时要先知道:这个元素现在到底有没有这个类?

怎么想: 把类名想成一个开关。contains('active') 不是在问“类名长什么样”,而是在问“这个元素身上有没有 active 这个类”。答案只有两个:true 或 false。

怎么做: 先拿到元素,再调用 classList.contains('类名')。

js
const hasActive = 元素.classList.contains('active');
console.log(hasActive); // true 或 false

易错点: contains 里写的是类名,不带点。写 contains('active'),不要写成 contains('.active')。返回值是布尔值,不要再和字符串比较。

contains 的基本用法:先判断再操作 ​

为什么需要: 如果当前元素已经有 active,再 add('active') 不会报错,但逻辑上可能重复;如果当前元素没有 active,直接 remove('active') 也不会报错,但你想做的可能是“没有就加上”。所以判断可以让逻辑更清楚。

怎么想: 先 contains 得到布尔值,再用 if 分流。

怎么做:

js
const li = document.querySelector('.item');

if (li.classList.contains('active')) {
  li.classList.remove('active');
} else {
  li.classList.add('active');
}

这段代码表达的是:有 active 就移除,没有就添加。它和 classList.toggle('active') 的效果接近,但这里重点是理解 contains 的判断作用。

易错点: classList 是元素上的属性,不是字符串方法。不要写成 元素.className.contains();className 是字符串,字符串的 contains 并不是这里要用的类名判断方法。

放到放大镜案例里理解 active 类 ​

为什么需要: 转写稿中,鼠标经过小图片时,需要让当前小图片的父级 li 有边框,其他 li 不要有边框。这就是典型的“先移除其他,再给当前添加”。

怎么想: 转写稿里的实际做法是先在小盒子 small 里找到已有的 .active,把它 remove 掉,再给当前图片的父级 li 添加 active。如果只想判断“当前这个 li 有没有 active”,就可以用 contains。

怎么做: 结合转写稿里的 active 操作,可以写成这样理解:

js
// 转写稿里的排他思路:先移除已有的 active
const activeLi = small.querySelector('.active');
if (activeLi) {
  activeLi.classList.remove('active');
}

// 再给当前小图片的父级 li 添加 active
e.target.parentNode.classList.add('active');

如果要判断当前 li 是否已经有 active,可以:

js
const li = e.target.parentNode;
console.log(li.classList.contains('active'));

易错点: 转写稿里反复强调:加边框的不是图片本身,而是图片的父级 li。所以判断类名时也要看准元素,不要拿 img 去判断 li 上的 active。另外,事件委托里用的是 mouseover,因为需要冒泡;mouseenter 不冒泡,不能直接拿来做事件委托。调试时先打印 e.target,确认当前经过的是不是 img,再拿 src,这是转写稿里已经验证过的过程。

调试时怎么确认 contains 的结果 ​

为什么需要: 类名操作出错时,靠猜很容易把 active、layer、large 看混。转写稿里也提醒过,layer 和 large 长得像,容易写错,所以命名要清楚。

怎么想: 先在控制台打印布尔值,确认“有没有类”,再决定 add 还是 remove。

怎么做:

js
console.log(元素.classList.contains('active'));

如果输出 true,说明有这个类;输出 false,说明没有。再配合 Elements 面板看元素上的 class 是否真的变化。

易错点: 不要把 contains 和 add、remove 混在一起用。contains 只负责判断,不负责添加或删除。还要注意类名拼写,active 不要写成 avtive;classList 不要写成 classlist。

小结 ​

本节的核心就是:元素.classList.contains('类名') 用来判断元素身上有没有某个类,有返回 true,没有返回 false。在转写稿的放大镜案例里,active 类控制小图片边框,实际落地是 remove 和 add;而 contains 补上了“先判断”的这一环。记住三点:类名不带点、判断的是元素本身、调试先打印布尔值。

课件图示 ​

课件第41页:步骤 小兔鲜页面注册 需求④: 下一步提交验证业务 1. 使用 submit 提

课件第48页:案例 小兔鲜首页页面 核心思路:判断本地存储有没有数据(用户名) 如果有就显示

代码示例 ​

示例一:放大镜业务一(小图 → 中图跟随变化 + 边框排他) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>放大镜 · 业务一</title>
<style>
  * { margin: 0; padding: 0; }
  li { list-style: none; }
  .picture { position: relative; width: 820px; height: 500px; margin: 40px auto; }

  /* 中盒子 */
  .middle { position: relative; width: 400px; height: 400px; cursor: move; }
  .middle img { display: block; width: 100%; height: 100%; }

  /* 遮罩层(业务二才用,先隐藏) */
  .layer { display: none; position: absolute; left: 0; top: 0;
           width: 200px; height: 200px; background: rgba(0, 0, 0, .4);
           pointer-events: none; }

  /* 大盒子 */
  .large { display: none; position: absolute; left: 420px; top: 0;
           width: 400px; height: 400px;
           background-repeat: no-repeat; background-size: 800px 800px; }

  /* 小盒子 */
  .small { position: absolute; left: 0; top: 420px; width: 400px; }
  .small ul { display: flex; gap: 12px; }
  .small li { border: 2px solid transparent; }
  .small li.active { border-color: #4caf50; }
  .small img { display: block; width: 70px; height: 70px; }
</style>
</head>
<body>
  <div class="picture">
    <!-- 中盒子:里面第 1 个孩子是图,第 2 个孩子是遮罩层 -->
    <div class="middle">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%23c0392b'/%3E%3C/svg%3E" alt="">
      <div class="layer"></div>
    </div>

    <!-- 大盒子 -->
    <div class="large"></div>

    <!-- 小盒子 -->
    <div class="small">
      <ul>
        <li class="active"><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%23c0392b'/%3E%3C/svg%3E" alt=""></li>
        <li><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%232980b9'/%3E%3C/svg%3E" alt=""></li>
        <li><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%2327ae60'/%3E%3C/svg%3E" alt=""></li>
        <li><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%23f39c12'/%3E%3C/svg%3E" alt=""></li>
        <li><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%238e44ad'/%3E%3C/svg%3E" alt=""></li>
      </ul>
    </div>
  </div>

<script>
(function () {
  // 防止变量污染,整个放大镜业务放进立即执行函数
  var small     = document.querySelector('.small');
  var middle    = document.querySelector('.middle');
  var middleImg = middle.children[0];   // 中盒子里第一张图

  // 1.1 事件委托:注册在小盒子身上
  small.addEventListener('mouseover', function (e) {
    // 只有经过的是图片才处理
    if (e.target.tagName !== 'IMG') return;

    // 1.2 把当前小图的地址给中图(三张图本就是同一张图)
    middleImg.src = e.target.src;

    // 1.3 排他:先清掉所有 li 的 active,再给当前图片的爸爸加 active
    var activeLi = small.querySelector('.active');
    if (activeLi) activeLi.classList.remove('active');
    e.target.parentNode.classList.add('active');
  });
})();
</script>
</body>
</html>

关键行与易错点

  • mouseover 有冒泡,才能做事件委托;mouseenter 没有冒泡,用在这里拿不到 e.target,是常见踩坑点。
  • e.target.tagName !== 'IMG' 必须判断:鼠标划过 ul 或 li 的空白处也会触发 mouseover,不判断就会报错。
  • middle.children[0] 取的是中盒子里第一个元素节点(children 返回伪数组),不是 childNodes。
  • 边框加在 li(图片的爸爸 parentNode) 上,不是加在 img 上;转写稿特意强调过这一点。
  • 排他思想:先 remove 再 add;判断是否已有类可用 classList.contains('active')。
  • 小图、中图、大图本质是同一张图,所以只要 src 一赋值,看起来就“换图”了。

示例二:放大镜完整版(显示隐藏 + 遮罩跟随 + 限定范围 + 大图跟随) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>放大镜 · 完整版</title>
<style>
  * { margin: 0; padding: 0; }
  li { list-style: none; }
  .picture { position: relative; width: 820px; height: 500px; margin: 40px auto; }

  /* 中盒子:cursor 写在这里,因为遮罩层被穿透了 */
  .middle { position: relative; width: 400px; height: 400px; cursor: move; }
  .middle img { display: block; width: 100%; height: 100%; }

  /* 遮罩层:pointer-events:none 让鼠标事件穿透 */
  .layer { display: none; position: absolute; left: 0; top: 0;
           width: 200px; height: 200px; background: rgba(0, 0, 0, .4);
           pointer-events: none; }

  /* 大盒子:背景图按原尺寸 800×800 显示,形成放大效果 */
  .large { display: none; position: absolute; left: 420px; top: 0;
           width: 400px; height: 400px;
           background-repeat: no-repeat; background-size: 800px 800px; }

  /* 小盒子 */
  .small { position: absolute; left: 0; top: 420px; width: 400px; }
  .small ul { display: flex; gap: 12px; }
  .small li { border: 2px solid transparent; }
  .small li.active { border-color: #4caf50; }
  .small img { display: block; width: 70px; height: 70px; }
</style>
</head>
<body>
  <div class="picture">
    <div class="middle">
      <img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%23c0392b'/%3E%3C/svg%3E" alt="">
      <div class="layer"></div>
    </div>

    <div class="large"></div>

    <div class="small">
      <ul>
        <li class="active"><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='800'%3E%3Crect width='800' height='800' fill='%23c0392b'/%3E%3C/svg%3E" alt=""></li>
        <li><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800

常见错误 ​

  • 现象:小图悬停无反应,或事件委托失效 → 原因:给 small 绑了 mouseenter,没有冒泡 → 修法:改用 mouseover,并判断 e.target.tagName === 'IMG'。
  • 现象:边框加到图片上,离开后状态混乱 → 原因:active 加给了 img,不是小图父级 li → 修法:先清理 small 内所有 li.active,再给 e.target.parentNode.classList.add('active')。
  • 现象:大盒子隐藏延迟异常,越滑定时器越多 → 原因:每次 mouseleave 都新建定时器,未清理旧定时器 → 修法:用外层 timerId,mouseenter 先 clearTimeout(timerId),mouseleave 再 setTimeout。
  • 现象:遮罩跟随鼠标时抖动、坐标跳变 → 原因:鼠标进入遮罩层,offsetX/offsetY 变成相对遮罩计算 → 修法:给遮罩加 pointer-events: none,事件仍绑在 middle 上。
  • 现象:遮罩跑出中盒子边界 → 原因:left/top 未做范围限制,或把 200 写死 → 修法:先算 maxX = middle.offsetWidth - layer.offsetWidth、maxY 同理,再用 Math.max/Math.min 夹取。
  • 现象:大图移动方向反了、速度不对或样式无效 → 原因:未用负两倍关系,或漏 px、background-position 格式错 → 修法:写成 backgroundPosition = &#96;\${-2 * x}px \${-2 * y}px&#96;。
  • 现象:鼠标移动样式不生效 → 原因:cursor: move 加在已设 pointer-events: none 的遮罩上 → 修法:把 cursor: move 加到 middle。

练习 ​

  1. 改错:现有代码给 small 绑定 mouseenter 并尝试事件委托,导致小图悬停无反应。请改为可用方案,并保留“经过小图才处理”的判断。
  2. 实现小图选中态:鼠标经过任意小图时,仅其父级 li 获得 active,其余 li 的 active 必须移除;写出关键 JS。
  3. 实现大盒子显隐:鼠标进入 middle 立即显示大盒子和遮罩,离开时遮罩立即隐藏、大盒子延迟 250ms 隐藏;要求页面最多只存在一个隐藏定时器。
  4. 实现遮罩约束:用 maxX/maxY 和 Math.min/Math.max 让遮罩只能在中盒子内移动,并写出 maxX 的计算式。
  5. 实现大图联动:已知中图 400×400、大图 800×800,中图内坐标为 x、y,写出大图 background-position 的设置代码,并保证方向与比例正确。

原视频对照 ​

#视频时长
1本次实战安排03:03
2放大镜案例-布局和业务分析05:07
3放大镜案例-鼠标经过小图片中图片跟随变化14:00
4放大镜案例-鼠标经过中盒子显示大盒子11:42
5放大镜案例-遮罩盒子跟随鼠标移动12:35
6放大镜案例-限定遮罩盒子范围06:28
7放大镜案例-大图片跟随移动05:13

小结 ​

  • 放大镜应按“小图→中图→大图”的链路拆分和排错,四个业务可独立验证。
  • 事件委托前先确认事件是否冒泡,mouseover 与 mouseenter 不能混用。
  • 类名控制选中态,display 控制显隐,left/top 控制遮罩,background-position 控制大图,职责要分清。
  • 遮罩坐标必须相对 middle 计算,pointer-events: none 是消除自遮挡抖动的关键。
  • 边界约束不要写死数值,先算 maxX/maxY 再夹取,适配不同尺寸更稳。
  • 定时器要“进入先清、离开再开”,避免重复创建和延迟异常。
  • 大图联动重点检查三件事:比例、方向、单位与格式。
  • 调试顺序建议:元素获取→事件触发→坐标/地址→样式写入。