Skip to content

2-5轮播图左右切换案例 ​

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

学习目标 ​

  • 能为左右切换按钮绑定 click 事件,使用索引变量实现顺序与倒序切换,并正确处理首尾边界复原。
  • 能获取并同步更新轮播图图片、文字、背景色和小圆点元素,完成一次完整切换效果。
  • 能运用排他思想,在切换小圆点时先移除旧 active 类,再为当前项添加 active 类。
  • 能使用 setInterval 实现自动播放,并通过按钮对象的 click() 方法模拟用户点击以复用切换逻辑。
  • 能使用 mouseenter 与 mouseleave 控制定时器的停止和重启,并通过保存定时器 ID 解决多次进入离开后定时器无法正确清除的问题。
  • 能识别左右按钮中的重复代码,抽取公共 toggle 函数并在两侧按钮事件中调用,验证功能不变且代码量减少。

知识点 ​

环境对象是什么 ​

为什么需要:函数体里经常出现 this。如果不弄清它指向谁,就容易把“当前按钮”“当前对象”写成固定变量,代码既不简洁也容易出错。
怎么想:this 不是固定值,而是函数运行时的“环境对象”。同一个函数,放在不同调用环境里,this 可能不同。
怎么做:先记住粗略规则:谁调用函数,this 就指向谁。再结合调用方式逐项判断。
易错点:不要脱离调用方式死记 this;判断前先看函数是“直接调用”“作为对象方法调用”,还是“作为事件回调调用”。

判断 this 的粗略规则 ​

为什么需要:实际代码里调用方式很多,需要一条可操作的判断入口。
怎么想:看函数名前面“点”前面是谁。obj.sing() 中 sing 前面是 obj,所以 this 指向 obj;事件回调由按钮触发,回调里的 this 指向按钮对象。
怎么做:在函数里临时写 console.log(this),逐场景验证。转写稿里的轮播图案例也体现了这一点:给按钮注册点击事件后,回调函数中的 this 指向被点击的按钮对象,所以可以用 this.style.backgroundColor 修改当前按钮样式,而不必反复写 btn1.style...。
易错点:window.fn() 和直接写 fn() 在本题环境下都相当于由 window 调用,所以普通函数里的 this 指向 window;但对象方法不能套成“函数名自己调用自己”。

全局环境与普通函数 ​

为什么需要:先确认最基础的全局环境,才能对比其他环境。
怎么想:全局作用域里直接输出的 this 是全局对象;普通函数直接调用时,相当于 window.函数()。
怎么做:

html
<body>
  <button>点击</button>
  <script>
    // 环境对象 this   粗略规则: 谁调用函数,this就指向谁

    // 1. 全局环境
    // console.log(this)  // this 指向 window 全局对象

    // 2. 普通函数
    function fn() {
      console.log(this)  // this 指向 window 全局对象
    }
    window.fn()

    // 3. 对象方法
    const obj = {
      uname: '佩奇',
      sing: function () {
        console.log(this)  // this 指向 obj对象
      }
    }
    obj.sing()

    // 4. 事件
    const btn1 = document.querySelector('button')
    btn1.addEventListener('click', function () {
      // console.log(this)  // this 指向 btn 这个对象
      // btn1.style.backgroundColor = 'pink'
      this.style.backgroundColor = 'pink'
    })
  </script>
</body>

易错点:看到 function fn(){} 不要立刻说 this 一定是 window,要看它最终怎样被调用;本例用 window.fn() 明确验证了普通函数的指向。

对象方法与事件处理函数 ​

为什么需要:对象方法和事件回调是 this 最常发挥作用的两个场景。
怎么想:对象方法看“点”前是谁;事件回调看“谁触发事件”。
怎么做:对象方法中 obj.sing() 的 this 是 obj;事件中 btn1.addEventListener('click', function(){ ... }) 的 this 是 btn1,因此可直接写 this.style.backgroundColor = 'pink'。转写稿中讲到 click() 方法可由程序模拟用户点击,这也说明点击事件最终仍作用在按钮对象上,回调中的 this 应围绕该按钮理解。
易错点:事件回调里如果继续写 btn1.style...,代码能跑但没体现环境对象的价值;若换成多个按钮,写死 btn1 就会出错。此时用 this 更合适。

调试与小结 ​

为什么需要:this 指向靠“看代码”容易误判,调试最可靠。
怎么想:在每个场景的 console.log(this) 处观察输出,再和调用方式对应。
怎么做:先验证全局 this,再验证 window.fn(),接着验证 obj.sing(),最后点击按钮验证事件中的 this。
易错点:不要把“谁调用,this 就是谁”当成万能公式背死;本节要掌握的是普通函数、对象方法、事件回调这三类环境。最终记住:全局和普通函数本例指向 window,对象方法指向 obj,事件回调指向触发事件的按钮对象。

一、排他思想是什么:只突出当前这一个 ​

排他思想是一种很常见的前端交互思路,目的不是“把所有元素都变亮”,而是在多个同类元素中,只让当前元素突出显示,其余元素恢复普通状态。比如有一组小圆点、一组按钮或一组列表项,当鼠标经过、点击或切换到某一项时,只有当前项添加高亮样式,其他项移除高亮样式。

排他思想示意图

为什么需要: 多个元素同时具备“选中/高亮”样式时,页面会失去焦点,用户不知道当前到底选中了谁。排他思想保证同一时刻只有一个“当前项”,视觉重点明确。

怎么想: 不要一上来就给当前元素加样式,而要先想“上一个高亮的是谁”。把旧的高亮状态清掉,再给当前元素加上高亮。也就是笔记里的口诀:

口诀:注意顺序
①:排除其他人
②:保留我自己

怎么做: 先用选择器找到已经带有高亮类名的元素,移除类名;再根据当前索引或当前元素,添加高亮类名。顺序不能反,否则容易出现“旧的没清掉、新的又加上”的双高亮。

易错点: 第一,顺序写反。第二,选择器没有限定范围,误删页面其他模块的同名类。第三,当前项索引从 0 开始,而 nth-child 从 1 开始,忘记 i + 1。第四,移除前没有判断是否真的存在高亮元素,初始状态可能没有 .active。

二、在轮播图小圆点中落地:先移除,再添加 ​

转写稿里的轮播图案例,是排他思想最典型的落点之一:切换图片时,小圆点也要跟着切换高亮。转写稿中反复强调,每次点击后要重新获取当前小圆点,因为这是“多个小 li 里选出我想要的那一个”,属于多选一。具体做法是:先找到上一个带 active 的小 li 并移除 active,再给当前索引对应的小 li 添加 active。

为什么需要: 轮播图有几张图就有几个小圆点,但当前图片只能对应一个小圆点。如果不移除旧圆点的 active,就会出现多个圆点同时亮起,用户无法判断当前播放到第几张。

怎么想: 把“当前索引 i”作为唯一信号。图片、文字、背景色、小圆点都围绕这个 i 变化。小圆点的排他分两步:先处理“上一个 active”,再处理“当前 i 对应的 li”。转写稿中的调试过程也说明:每次点击后都要重新获取小 li,再通过 nth-child(i + 1) 选中当前项。

怎么做: 可参考下面这段按转写稿类名整理的示意代码:

js
// ① 排除其他人:先找到上一个带 active 的小 li 并移除
const activeLi = document.querySelector('.slide .active');
if (activeLi) {
  activeLi.classList.remove('active');
}

// ② 保留我自己:给当前索引对应的小 li 添加 active
document.querySelector(`.slide li:nth-child(${i + 1})`).classList.add('active');

这里的 i 是当前图片索引。转写稿中右侧按钮是 i++,左侧按钮是 i--;到边界时还要复原,例如右侧用 i = i % slideData.length,左侧用 i = (i + slideData.length) % slideData.length。这些复原操作保证 i 始终是合法索引,排他高亮才能选到正确的小圆点。

易错点: 第一,先 add 再 remove,可能把刚加上的 active 又删掉,或者造成样式闪烁。第二,nth-child 忘记 i + 1,因为数组索引从 0 开始。第三,移除时选择器写成范围过大的 .active,可能误伤其他模块。第四,左侧按钮减到第一张之前,i 会变成 -1,转写稿调试时明确看到 i 为负一,数组找不到负索引会报错,所以必须先做边界复原。第五,没有重新获取当前小 li,导致选中的还是旧元素。

三、把排他思想记成固定动作 ​

排他思想不难,难在顺序和范围。写的时候可以默念:先找旧高亮,移除;再找当前项,添加。 轮播图里还要记住:当前索引 i 是信号灯,边界要复原,选择当前小圆点时注意 i + 1。转写稿最后把左右按钮中重复的“更换图片、文字、背景、小圆点”抽成 toggle 函数,也说明排他逻辑适合封装复用:谁调用,谁执行,但内部仍然是“先排除其他人,再保留我自己”。

易错点总结: 顺序别反,范围别大,索引别错,边界别漏。掌握这四个点,排他思想就能稳定用于小圆点、导航高亮、列表选中、按钮切换等场景。

课件图示 ​

课件第39页:描述 属性/方法 效果 事件监听 元素.addEventListener() 事

课件第42页:步骤 Tab栏切换 业务1分析:鼠标经过谁,谁会高亮显示 ①:给5个链接注册鼠标

代码示例 ​

示例一:环境对象 this(谁调用,this 就是谁)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>环境对象 this</title>
</head>
<body>
  <button>点击</button>
  <script>
    // 1. 全局环境
    // console.log(this)  // this 指向 window 全局对象

    // 2. 普通函数
    function fn() {
      console.log(this)  // this 指向 window 全局对象
    }
    window.fn()

    // 3. 对象方法
    const obj = {
      uname: '佩奇',
      sing: function () {
        console.log(this)  // this 指向 obj 对象
      }
    }
    obj.sing()

    // 4. 事件
    const btn1 = document.querySelector('button')
    btn1.addEventListener('click', function () {
      // console.log(this)  // this 指向 btn1 这个对象
      // btn1.style.backgroundColor = 'pink'
      this.style.backgroundColor = 'pink'   // 等价于 btn1.style.xxx
    })
  </script>
</body>
</html>

要点解释:

  • window.fn() 与直接写 fn() 完全等价,所以普通函数里的 this 是 window;直接调用函数其实相当于 window.函数。
  • obj.sing() 中函数被 obj 调用,所以 this 是 obj,这是"谁调用,this 就是谁"这条粗略规则的典型体现。
  • 事件处理函数里的 this 指向绑定事件的那个元素(这里是 btn1),所以 this.style.backgroundColor = 'pink' 可以替代 btn1.style.backgroundColor,代码更简洁。
  • 易错点:如果把事件回调改成箭头函数,this 就不再是按钮元素了;本节一律使用 function () {} 写法。

示例二:轮播图点击切换版(左右按钮 + 自动播放 + 鼠标经过暂停 + 抽取公共函数)

js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>轮播图 - 点击切换版</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    ul { list-style: none; }
    .slider {
      width: 720px;
      height: 400px;
      margin: 80px auto;
      position: relative;
      overflow: hidden;
      background-color: #e6f7ff;   /* 背景色由 JS 更换 */
      transition: background-color .3s;
    }
    .slide_wrapper img { width: 720px; height: 400px; display: block; }
    .slide_footer {
      position: absolute;
      left: 0;
      bottom: 0;
      width: 100%;
      height: 40px;
      line-height: 40px;
      text-align: center;
      color: #fff;
      background: rgba(0, 0, 0, .35);
    }
    .toggle {
      position: absolute;
      top: 50%;
      left: 0;
      width: 100%;
      transform: translateY(-50%);
      display: flex;
      justify-content: space-between;
      padding: 0 12px;
    }
    .toggle a {
      width: 36px;
      height: 60px;
      line-height: 60px;
      text-align: center;
      text-decoration: none;
      color: #fff;
      font-size: 24px;
      background: rgba(0, 0, 0, .3);
      border-radius: 4px;
      cursor: pointer;
    }
    .slider ul {
      position: absolute;
      left: 0;
      bottom: 50px;
      width: 100%;
      display: flex;
      justify-content: center;
      gap: 8px;
    }
    .slider li {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      background: rgba(255, 255, 255, .6);
    }
    .slider li.active { background: #fff; transform: scale(1.3); }
  </style>
</head>
<body>
  <div class="slider">
    <div class="slide_wrapper">
      <img src="" alt="">
    </div>
    <div class="slide_footer">
      <p></p>
    </div>
    <div class="toggle">
      <a href="javascript:;" class="prev">&lt;</a>
      <a href="javascript:;" class="next">&gt;</a>
    </div>
    <ul>
      <li class="active"></li>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  </div>

  <script>
    // 0. 数据(真实项目里把 url 换成图片地址即可)
    const svg = (n, c) =>
      `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="720" height="400"><rect width="720" height="400" fill="${c}"/><text x="360" y="235" font-size="120" font-family="sans-serif" text-anchor="middle" fill="%23fff">${n}</text></svg>`;

    const slideData = [
      { url: svg(1, '%23ff9a9e'), title: '第 1 张:佩奇', color: '#ffe4e9' },
      { url: svg(2, '%23a1c4fd'), title: '第 2 张:乔治', color: '#e3f0ff' },
      { url: svg(3, '%23b7f8db'), title: '第 3 张:猪爸爸', color: '#e6fbf3' },
      { url: svg(4, '%23fbc2eb'), title: '第 4 张:猪妈妈', color: '#fdeef8' }
    ];

    // 1. 获取元素(只获取一次)
    const img = document.querySelector('.slide_wrapper img');
    const p = document.querySelector('.slide_footer p');
    const slider = document.querySelector('.slider');
    const next = document.querySelector('.toggle .next');
    const prev = document.querySelector('.toggle .prev');

    // 2. 控制信号量
    let i = 0;

    // 3. 抽取公共函数:更换图片、文字、背景色、小圆点
    function toggle() {
      img.src = slideData[i].url;                       // 换图片
      p.innerHTML = slideData[i].title;                 // 换文字
      slider.style.backgroundColor = slideData[i].color; // 换背景色

      // 小圆点:排他思想——先排除别人,再保留自己
      const old = document.querySelector('.slider li.active');
      if (old) old.classList.remove('active');
      document.querySelector(`.slider li:nth-child(${i + 1})`).classList.add('active');
    }

    // 4. 需求一:点击右侧按钮,顺序无缝播放
    next.addEventListener('click', function () {
      i++;
      i = i % slideData.length;   // 到了最后一张,复原到 0
      toggle();
    });

    // 5. 需求二:点击左侧按钮,倒序无缝播放
    prev.addEventListener('click', function () {
      i--;
      i = (i + slideData.length) % slideData.length;  // 到 -1 时回到最后一张
      toggle();
    });

    // 6. 需求三:开启定时器自动播放(程序模拟点击右侧按钮)
    let timerId = setInterval(function () {
      next.click();
    }, 1000);

    // 7. 需求四之一:鼠标经过大盒子,停止定时器
    slider.addEventListener('mouseenter', function () {
      clearInterval(timerId);
    });

    // 8. 需求四之二:鼠标离开大盒子,重新开启定时器
    slider.addEventListener('mouseleave', function () {
      clearInterval(timerId);          // 先清一次,避免定时器越开越多
      timerId = setInterval(function () {
        next.click();
      }, 1000);
    });

    // 9. 首次渲染第一张
    toggle();
  </script>
</body>
</html>

要点解释:

  • 第 3 步的 toggle() 就是转写稿里"抽风"的那一步:左右按钮里"换图片、换文字、换背景色、换小圆点"这 5 句完全相同,抽成一个函数后两边各调一次,代码量减半。
  • 小圆点用的是排他思想,口诀是"① 排除其他人,② 保留我自己":先用 .slider li.active 找到上一个高亮的 li 并 classList.remove('active'),再用模板字符串 .slider li:nth-child(\${i + 1}) 给当前项 classList.add('active')。顺序不能反,且每次都要重新 querySelector 获取,因为高亮的位置在变。
  • 右侧按钮复原用取余:i = i % slideData.length,到最后一张再点就回到 0,实现无缝衔接。
  • 左侧按钮复原用 i = (i + slideData.length) % slideData.length。易错点:括号不能省,写成 i + slideData.length % slideData.length 会先算取余,结果全错;另外必须"先加长度再取余",因为 -1 + 4 = 3、3 % 4 = 3,正好是最后一张的索引;而正常索引如 3,(3 + 4) % 4 仍是 3,不会受影响。
  • next.click() 是程序模拟用户点击,所以定时器里不用复制一大段业务代码;但要注意这种方式只适用于 click、focus、blur 这类事件,鼠标经过之类的不能这样模拟。
  • mouseenter / mouseleave 要绑在 .slider 这个大盒子上,而不是图片或按钮上,否则鼠标在盒内移动就会误触发。
  • 最容易错的一点:mouseleave 里必须先 clearInterval(timerId) 再开新定时器,并且把新定时器重新赋给 timerId(所以用 let 声明)。否则每次离开都新开一个定时器而旧 ID 已被覆盖、清不掉,播放会越来越快。

常见错误 ​

1. 点到最后一张再点「下一张」,图片卡住或控制台报错

  • 现象:轮播到第 8 张后再点右侧按钮,图片不再切换,报 Cannot read properties of undefined。
  • 原因:索引一直自增到 8,而数组最大索引是 7,取不到对象。
  • 修法:加完立刻复原,i = i % arr.length(或 if (i >= arr.length) i = 0),复原必须写在「取数据之前」。

2. 第一张再点「上一张」,图片消失或报错

  • 现象:在首张点左侧按钮,图片变成空白/报错,控制台能打印出 -1。
  • 原因:i-- 后变成负数,负索引取不到数组元素。
  • 修法:i = (i + arr.length) % arr.length,括号不能省——写成 i + arr.length % arr.length 会先算取余,结果仍是 -1。

3. 小圆点越点越多,多个同时高亮

  • 现象:连续点几次,小圆点出现两三个 active 样式。
  • 原因:只做了「保留我自己」,漏了「排除其他人」;或先加后删,把自己也删掉了。
  • 修法:严格按「①排除其他人 → ②保留我自己」的顺序写,先 querySelector('.active').classList.remove('active') 再给当前项 classList.add('active');同时注意每次点击都要重新获取小圆点集合,不能用缓存的那一份。

4. 鼠标反复进出后,轮播越转越快甚至停不下来

  • 现象:鼠标在轮播图上划几下,切换速度翻倍,最后 clearInterval 关不掉。
  • 原因:mouseleave 里新开了定时器,但没有把新 ID 存回变量;mouseenter 清的还是旧 ID,旧定时器一直活着并不断累积。
  • 修法:保存 ID 的变量用 let(要允许重新赋值),且开新定时器前先清旧的:clearInterval(timerId); timerId = setInterval(...)。

5. 在事件回调里用 this 拿到 window 而不是按钮

  • 现象:this.style.background = 'pink' 报错,console.log(this) 打印 Window。
  • 原因:回调写成了箭头函数,箭头函数不绑定自己的 this,沿外层作用域取到 window。
  • 修法:需要 this 的地方用 function () {};确实要用箭头函数就直接写 btn / e.currentTarget,别依赖 this。

6. 抽取公共函数后,点击按钮没反应

  • 现象:把更换图片那段抽成 toggle() 后,左右按钮点击都没效果,但函数本身不报错。
  • 原因:只定义了函数没调用;或函数里用到的索引变量 i 被写在了函数内部,每次调用都重置成 0。
  • 修法:在左右两个回调里各补一次 toggle() 调用;索引 i 声明在函数外层的公共作用域,函数内只负责读取和渲染。

7. 定时器自动播放写成「复制一整段业务代码」

  • 现象:定时器里塞了几十行,后来改需求要改两处,左右按钮和自动播放不同步。
  • 原因:没意识到「自动播放 ≈ 每隔一秒程序点一次右按钮」。
  • 修法:定时器里只写 next.click(),让程序模拟点击,业务逻辑只保留一份(并进一步抽成 toggle)。

练习 ​

练习 1(定位 this) 不改动任何一行输出语句,先写出下列四段代码各自打印什么,再上机验证:

js
function a() { console.log(this) }
const o = { m: function () { console.log(this) } }
const b = o.m
document.querySelector('button').addEventListener('click', () => console.log(this))

最后补一题:把 b() 改成 o.m(),结果为什么不同?

练习 2(排他思想) 页面上有 5 个 li,鼠标经过谁,谁变红、其余变灰。要求:只用「先排他、后保留」两步写;写完后故意把两步顺序调换,观察并解释现象差异。

练习 3(首尾无缝) 基于课上轮播图,把右侧按钮的复原改成 if 判断、把左侧按钮的复原改成取余,两种写法各写一遍,验证效果完全一致;再故意删掉左侧的括号,记录报错与结果。

练习 4(暂停与恢复) 给轮播图加上鼠标移入暂停、移出继续。要求:在控制台打印每次新建定时器的 ID,鼠标进出 5 次,检查 ID 是否连续递增、是否只剩一个定时器在跑(可用 console.log + 手动 clearInterval 验证)。

练习 5(重构) 把左右按钮中重复的「换图 + 换文字 + 换背景 + 换小圆点」抽成 toggle(),要求:左右按钮各只剩「改变量 + 复原 + 调 toggle」三行;然后让定时器也只调 next.click()。完成后统计代码行数减少了多少。

原视频对照 ​

#视频时长
1轮播图切换版-右侧按钮需求制作12:16
2轮播图切换版-左侧按钮需求制作11:11
3轮播图切换版-自动播放轮播图04:24
4轮播图切换版-自动播放轮播图09:21
5轮播图切换版-抽取公共toggle函数06:14

小结 ​

  1. this 的判定先看调用方式而不是定义位置:window.fn()、obj.fn()、btn 的事件回调,指向各不相同。
  2. 「谁调用,this 就是谁」只是粗略规则,遇到箭头函数、回调传参就要立刻放弃这条规则,改看执行上下文。
  3. 轮播图的所有 bug 几乎都集中在索引越界上,加完或减完必须立刻复原,且复原要放在取数组元素之前。
  4. 取余公式 (i + len) % len 一个式子同时解决上界和下界,比 if 分支更统一,但要记得加括号。
  5. 排他思想的顺序是硬性的:先排除其他人、再保留自己,顺序颠倒等于白做。
  6. 定时器是「有状态」的资源:谁开的谁负责关,ID 必须存进可重新赋值的变量,开新之前先关旧,否则一定越转越快。
  7. 元素.click() 让程序代替用户点击,是复用业务逻辑最省事的手段,但它只对 click/focus/blur 这类事件有效,鼠标移入移出模拟不了。
  8. 案例做完不等于结束:把重复代码抽成函数,是这个案例里唯一一次「回头看」的优化,也是以后写大案例的固定收尾动作。