主题
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 函数,也说明排他逻辑适合封装复用:谁调用,谁执行,但内部仍然是“先排除其他人,再保留我自己”。
易错点总结: 顺序别反,范围别大,索引别错,边界别漏。掌握这四个点,排他思想就能稳定用于小圆点、导航高亮、列表选中、按钮切换等场景。
课件图示


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