当前位置: 首页 > news >正文

回调函数、作用域与闭包:从图片预览案例深入理解

回调函数、作用域与闭包:从图片预览案例深入理解

参考深度解析:JavaScript变量声明的演变与核心差异(var/let/隐式声明)

引言

在前端开发中,回调函数、作用域和闭包是三个紧密相关的概念,也是很多初学者容易混淆的地方。本文将通过一个实际的图片预览切换案例,详细剖析这些概念,帮助你掌握前端进阶必备知识。

案例介绍:图片预览切换功能

我们先来了解一下这个图片预览切换功能:

  • 页面上有一个大图展示区域和下方的小图列表
  • 鼠标悬停在任一小图上,大图区域会切换为对应的图片
  • 当前选中的小图会显示红色边框

效果预览

代码如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>大小图预览</title><style>*{margin: 0;padding: 0;box-sizing: border-box;}html,body,#page {width: 100%;height: 100%;background-color: rgba(0, 0, 0, .4);}/* 防止.display外边距穿透 *//* 法一 *//*#page {padding: 1px;}*//* 法二 */#page::before {content: '';display: block;height: 200px;}.display {background-color: rgb(255, 192, 203,.8);width: 430px;/* 430px大图100px小图20px间隔 */height: 550px;/* 法一margin设置 *//* margin: 200px auto; *//* 法二margin设置 */margin: 0 auto;background: url(./pic1.jpg) no-repeat;background-size: 430px 430px;}ul {list-style: none;/* 消除影响布局的空白换行 */font-size: 0;}ul::before {content: "";display: block;height: 430px;}ul>li {display: inline-block;}ul>li:not(:last-child) {margin-right: 10px;}ul>li>img {width: 100px;height: 100px;object-fit: cover;/* 透明边框留给active空间 */border: 1px solid transparent;cursor: pointer;}.active {border: 1px solid red;}</style>
</head>
<body onload="onLoad()"><div id="page"><div class="display"><ul><li><img src="./pic1.jpg" alt="1"></li><li><img src="./pic2.jpg" alt="2"></li><li><img src="./pic3.jpg" alt="3"></li><li><img src="./pic4.jpg" alt="4"></li></ul></div></div>
</body>
<script>onLoad = ()=>{var imgs = document.getElementsByTagName("img");var displayBox = document.getElementsByClassName("display")[0];var img1 = imgs[0];// console.log(img1.classList);// console.log(img1.className);  // img1.setAttribute("class",`${img1.classList} active`);img1.classList.add("active");for(let i=0;i<imgs.length;i++){imgs[i].onmouseover = ()=>{for(let j=0;j<imgs.length;j++){imgs[j].classList.remove("active");}imgs[i].classList.add("active");var imgSrc = imgs[i].getAttribute("src");displayBox.style.background = `url(${imgSrc}) no-repeat`;displayBox.style.backgroundSize = `430px 430px`;}}}
</script>
</html>

一、回调函数详解

什么是回调函数?

回调函数是作为参数传递给另一个函数,并在特定时机被调用的函数。简单来说,就是"把函数A传给函数B,B在合适的时机调用A"。常用作事件触发函数。

在我们的案例中,每个图片的onmouseover事件绑定的函数就是典型的回调函数:

imgs[i].onmouseover = ()=>{// 这段代码不会立即执行// 只有当鼠标移到图片上时才会被调用
}

回调函数的特点

  1. 延迟执行:定义时不执行,只有在特定条件满足时才执行
  2. 控制反转:将控制权交给调用者,由调用者决定何时执行
  3. 常见场景:事件处理、异步操作、定时器等

案例中的回调函数分析

在案例中,我们为每个图片元素绑定了一个鼠标悬停事件的回调函数。这些函数不会在页面加载时立即执行,而是在用户将鼠标移到对应图片上时才会被调用。

二、作用域深度解析

作用域的定义与类型

作用域决定了变量的可访问范围。JavaScript中主要有三种作用域:

  1. 全局作用域:在代码中任何地方都能访问
  2. 函数作用域:只在函数内部可访问(var声明的变量)
  3. 块级作用域:只在代码块内可访问(let/const声明的变量)

var与let的关键区别

// var的函数作用域
function demo1() {for(var i=0; i<3; i++) {console.log(i); // 0,1,2}console.log(i); // 3 (i在循环外仍可访问)
}// let的块级作用域
function demo2() {for(let j=0; j<3; j++) {console.log(j); // 0,1,2}console.log(j); // 报错:j未定义
}

案例中的作用域分析

在我们的图片预览案例中,关键的作用域问题出现在这里:

for(let i=0; i<imgs.length; i++){imgs[i].onmouseover = ()=>{// 这里使用了i变量imgs[i].classList.add("active");}
}

这里使用let i声明,每次循环迭代都创建一个新的块级作用域变量i。如果改为var i,则会出现问题,因为回调函数执行时i的值已经变成了imgs.length

三、闭包详解

什么是闭包?

闭包是指函数可以"记住"并访问它定义时的外部变量,即使该函数在其原始作用域之外执行。

简单来说,闭包就是一个函数和它所引用的外部变量环境的组合。

闭包的工作原理

当一个函数在另一个函数内部定义,并且内部函数引用了外部函数的变量时,内部函数就会形成闭包,即使外部函数已经执行完毕。

function createCounter() {let count = 0;  // 外部函数的变量return function() {  // 内部函数形成闭包return ++count;  // 引用外部函数的变量};
}const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

案例中的闭包分析

在我们的图片预览案例中,每个鼠标悬停事件的回调函数都形成了闭包:

for(let i=0; i<imgs.length; i++){imgs[i].onmouseover = ()=>{// 形成闭包,"记住"了创建时的i值imgs[i].classList.add("active");var imgSrc = imgs[i].getAttribute("src");displayBox.style.background = `url(${imgSrc}) no-repeat`;}
}

每个回调函数"记住"了它被创建时对应的i值,这就是闭包的作用。

四、为什么 var 和 let 会产生不同结果?

var的问题:共享同一个变量

如果我们将代码改为使用var

for(var i=0; i<imgs.length; i++){imgs[i].onmouseover = ()=>{imgs[i].classList.add("active");// ...}
}

这会导致以下问题:

  1. 循环结束后,i的值为imgs.length(例如4)
  2. 当用户将鼠标悬停在任何图片上,触发回调函数时
  3. 回调中访问的i都是同一个变量,值为4
  4. imgs[4]不存在,所以会得到undefined

这段代码会在控制台报错:Cannot read property 'classList' of undefined

let的解决方案:每次迭代创建新变量

使用let时:

  1. 每次循环迭代,都会创建一个新的块级作用域变量i
  2. 每个回调函数会捕获(形成闭包)它所在迭代的i值(0, 1, 2, 3…)
  3. 当回调执行时,使用的是各自捕获的i

这就解释了为什么使用let可以正确工作,而var会导致问题。

五、箭头函数与普通函数的影响

在我们讨论的闭包和作用域问题中,使用箭头函数()=>{}或普通函数function(){}不会影响结果:

// 两者在闭包行为上相同
for(let i=0; i<imgs.length; i++){// 箭头函数写法imgs[i].onmouseover = () => {imgs[i].classList.add("active");}// 普通函数写法 - 效果相同imgs[i].onmouseover = function() {imgs[i].classList.add("active");}
}

它们的主要区别在于this的指向,而非闭包特性。

六、最佳实践总结

  1. 循环中创建回调函数时,使用let而非var

    for(let i=0; i<arr.length; i++){element.addEventListener('click', ()=>{console.log(i);});
    }
    
  2. 了解回调执行的时机
    回调函数不是在定义时执行,而是在触发事件时执行

  3. 避免过度使用闭包
    闭包会占用内存,过度使用可能导致性能问题

  4. 合理使用块级作用域
    使用let/const创建块级作用域,避免变量污染

结语

通过这个图片预览切换案例,我们深入了解了回调函数、作用域和闭包这三个紧密相连的概念。理解它们的关系,是成为高级前端开发者的必经之路。希望这篇文章能帮助你更深入地理解这些概念,提升前端开发技能!


实践练习:

  1. 尝试将案例中的let i改为var i,看看会发生什么?
  2. 如何使用forEach或其他数组方法重写这段代码?
  3. 如何在不使用闭包的情况下实现相同功能?

实践练习答案

1. 将案例中的let i改为var i,看看会发生什么?

如果将代码改为:

for(var i=0; i<imgs.length; i++){imgs[i].onmouseover = ()=>{for(let j=0; j<imgs.length; j++){imgs[j].classList.remove("active");}imgs[i].classList.add("active"); // 这里会出错var imgSrc = imgs[i].getAttribute("src");displayBox.style.background = `url(${imgSrc}) no-repeat`;displayBox.style.backgroundSize = `430px 430px`;}
}

结果:

  • 当鼠标移到任何图片上时,浏览器会报错:Cannot read properties of undefined (reading 'classList')
  • 原因:循环结束后,i的值变为4(即imgs.length),而imgs[4]不存在
  • 所有回调函数引用的都是同一个i,值为4,因此imgs[i]会是undefined

2. 如何使用forEach或其他数组方法重写这段代码?

onLoad = ()=>{var imgs = document.getElementsByTagName("img");var displayBox = document.getElementsByClassName("display")[0];var img1 = imgs[0];img1.classList.add("active");// 将HTMLCollection转为数组,然后使用forEachArray.from(imgs).forEach((img) => {img.onmouseover = () => {// 清除所有active样式Array.from(imgs).forEach((item) => item.classList.remove("active"));// 为当前图片添加active样式img.classList.add("active");// 更新大图var imgSrc = img.getAttribute("src");displayBox.style.background = `url(${imgSrc}) no-repeat`;displayBox.style.backgroundSize = `430px 430px`;}});
}

这种方法更简洁,不需要使用索引,直接使用当前元素引用,避免了索引问题。

3. 如何在不使用闭包的情况下实现相同功能?

方法一:使用事件委托

onLoad = ()=>{var imgs = document.getElementsByTagName("img");var displayBox = document.getElementsByClassName("display")[0];var img1 = imgs[0];img1.classList.add("active");// 获取ul元素const ul = document.querySelector("ul");// 使用事件委托,将事件监听器添加到父元素ul.addEventListener("mouseover", function(e) {// 检查事件来源是否为img元素if(e.target.tagName === "IMG") {// 移除所有active类for(let j=0; j<imgs.length; j++){imgs[j].classList.remove("active");}// 给当前元素添加active类e.target.classList.add("active");// 更新大图var imgSrc = e.target.getAttribute("src");displayBox.style.background = `url(${imgSrc}) no-repeat`;displayBox.style.backgroundSize = `430px 430px`;}});
}

方法二:使用自定义数据属性

onLoad = ()=>{var imgs = document.getElementsByTagName("img");var displayBox = document.getElementsByClassName("display")[0];var img1 = imgs[0];img1.classList.add("active");for(let i=0; i<imgs.length; i++){imgs[i].onmouseover = function() {// 使用this引用当前元素,而不是通过闭包捕获的ifor(let j=0; j<imgs.length; j++){imgs[j].classList.remove("active");}this.classList.add("active");var imgSrc = this.getAttribute("src");displayBox.style.background = `url(${imgSrc}) no-repeat`;displayBox.style.backgroundSize = `430px 430px`;}}
}

这两种方法都避免了依赖闭包捕获循环变量,前者通过事件委托减少了事件监听器的数量,后者通过使用this关键字直接引用当前元素。

http://www.lqws.cn/news/552691.html

相关文章:

  • 文件管理与Java操作全解析
  • 编译安装detectron2
  • 常用工具库
  • 北大肖臻《区块链技术与应用》学习笔记
  • 智能库室管控系统DW-S306|全国已经规模化应用
  • 微服务项目,启动某服务,编译后就没反应
  • 【JS-6-ES6中的let和const】深入理解ES6中的let和const:块级作用域与变量声明的新范式
  • 【数据标注师】意图标注
  • 力扣网C语言编程题:在数组中查找目标值位置之二分查找法
  • 能否仅用两台服务器实现集群的高可用性??
  • ADVANCED INTELLIGENT SYSTEMS 东京大学仿生人类手指机器人,实现“皮肤”补水!
  • Harbor的安装与使用
  • 基于MFC的遥感图像匹配程序设计
  • Java 识别和处理 HTML 标签内容
  • Solidity学习 - ABI 应用二进制接口
  • 叉车考试真题(含答案)pdf下载
  • 权限提升-工作流
  • React用户交互事件
  • 一款支持多日志器、多级别、多落地方式的同异步日志系统
  • ViewModel 使用总结:普通、Shared 及嵌套 Fragment 场景
  • 栅极驱动器选的好SiC MOSFET高效又安全
  • RabbitMQ-基础篇
  • StarRocks 向量索引如何让大模型“记性更好”?
  • 【Linux】理解进程状态与优先级:操作系统中的调度原理
  • linux安装vscode
  • ABP VNext + 多数据库混合:SQL Server+PostgreSQL+MySQL
  • .NET C# async/定时任务的异步线程池调度方案最大线程数‌ = 处理器核心数 × 250
  • python 文件处理工具(包含文件读写、后缀获取、压缩和解压、文件夹遍历等)
  • C++ STL深度剖析:Stack、queue、deque容器适配器核心接口
  • [Linux]从零开始的STM32MP157移植Ubuntu根文件系统教程