前言
最近在学JS,在做一个用户通过点击星星来进行量化评价的小案例时,出现了点小问题,我把我的案例简化一下聚焦在我出现的小问题上。
这里要实现的效果就是用户将鼠标悬停在星星图案上,悬停的星星以及它左边的星星图案都会被点亮。
鼠标点击星星图案后,星星悬停点亮效果将不会出现,
然后点击按钮一个循环确认星星数,即可提交评价。
具体效果可以参照下图
这不小菜一碟吗?但在实现这个案例时,我却犯了一个致命性的错误。
先用HTML和CSS写出静态内容
这里面的star0.png表示星星熄灭状态的图片,
star1.png则表示星星点亮状态的图片。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>五星好评</title>
<style>
.main{
margin: 0px;
padding: 0px;
}
#stars{
border-radius: 0.5rem;
width:100%;
height:auto;
}
img{
width:100px;
height:100px;
}
![请添加图片描述](https://img-blog.csdnimg.cn/direct/b2495e8734e0484e856f7ea3dadd2743.gif)
</style>
</head>
<body>
<div class="main">
<div id="stars_label">
<img src="star0.png">
<img src="star0.png">
<img src="star0.png">
<img src="star0.png">
<img src="star0.png">
</div>
<input type="button" value="提交评价" id="btn">
<label id="display">
<strong>您的评价是:<span id="assess"></span></strong>
</label>
</div>
</body>
</html>
编写JS代码实现动态效果
我最开始的思路是:
在窗口加载完毕后,开始循环这五个img元素的onmouseover
(鼠标悬停)事件。
一旦鼠标在某个img
元素上悬停,就会根据悬停元素在img元素集合的索引,通过替换src属性实现图片切换达到点亮效果。
在悬停时,不会立即触发图片切换按钮,而是先通过unsure判断是否有进行过img
元素单击事件(案例中规定鼠标单击后就表示已确定评价,固定点亮星星数),如已经进行,则无需实现悬停效果。
<script>
//获取到五个图片元素的集合
starImgs=document.getElementsByTagName("img");
mySpan=document.getElementById("assess");
myBtn=document.getElementById("btn");
window.onload=function(){
//设定初始时,未确定评价状态为真
unsure=true;
for(var index=0;index<starImgs.length;index++){
starImgs[index].onmouseover=function(){
if(unsure){//判断是否为未确定评价状态
lightStars(index);//根据索引触发切换图片的函数
}
}
}
//单击星星事件,表示确认评价
for(let index=0;index<starImgs.length;index++){
starImgs[index].onclick=function(){
sureStars(index);
}
}
}
function lightStars(img_index){
for(var i=0;i<=img_index;i++){
//点亮从左至悬停处的星星
starImgs[i].src="star1.png"; }
for(;i<starImgs.length;i++){
// 熄灭悬停处右边的星星
starImgs[i].src="star0.png";
}
}
function sureStars(stars){
unsure=false;//设置为false,即已确定评价状态取消鼠标悬停触发的函数
starImgs.data=stars+1;//设置数据,也就是分数
}
myBtn.onclick=function(){
mySpan.innerHTML=starImgs.data+"颗星星";
}
<script>
代码问题
这个思路虽然是笨了一点,但按道理来说实现案例效果是没有问题的。
在浏览器加载网页的时候,我们可以看到,它并没有我文章开头展示的效果,而且JS还报了错。
具体报错图片
一般报这个错我自己的经验就是两种原因,
要么就是对象压根就没这个属性,无法更改该属性;
要么是对象压根没正确获取;
进入报错行:
我可以保证我的img元素是被正常获取到的,所以src这个属性是绝对会有的,
那么只可能是第二个原因了,没有正常获取到对象,也就是说starImgs[i]不存在。
进入调试:
我发现,无论我的鼠标悬停到哪个星星上,它传给img_index的值永远是5,这明显有问题啊!一共就五颗星星,按照索引来说,就算是悬停到第五颗星星的地方,传入的参数值也只能是4啊
找到传入参数值的地方:
乍一看,我还真没看出什么问题来,
所以开始我是拿foreach以另一种方式去解决这个传入参数值总是5的问题,也确实轻轻松松地就解决了。
后来我越想越不对劲,又看了看这段已经要被我删除的代码,终于看到了出错的地方。
for(var index=0;index<starImgs.length;index++){
starImgs[index].onmouseover=function(){
if(unsure){//判断是否为未确定评价状态
lightStars(index);//根据索引触发切换图片的函数
}
}
}
出错问题类型
代码中存在一个常见问题,这与循环中的 index 变量的闭包有关。
在JavaScript中,闭包捕获的是变量最终值,而不是闭包创建时变量的值。因此,当 onmouseover
事件触发时,它总是引用 index 的最后一个值,而不是创建事件处理程序时的值。
什么是闭包
通俗点来讲,当一个函数内部定义的函数可以访问到外部函数的变量时,这个内部函数就是一个闭包。闭包就像是一个背包,可以携带着外部函数的变量,即使外部函数已经执行结束了,内部函数仍然可以使用这些变量。所以,闭包可以让我们在函数外部访问函数内部的数据,就像是在一个安全的“背包”里面携带着需要的东西一样。
代码具体问题
for(var index=0;index<starImgs.length;index++){
starImgs[index].onmouseover=function(){
if(unsure){//判断是否为未确定评价状态
lightStars(index);//根据索引触发切换图片的函数
}
}
}
当在循环中创建闭包时,闭包内部的函数(在这里是 onmouseover
事件处理函数)引用的是循环结束后的变量值。在给每个图片元素绑定onmouseover
事件处理函数时,index 的值在每次循环中都会递增,并且闭包(onmouseover 事件处理函数
)会捕获到最终的 index 值。
当循环结束后,index 的值会变成 5,因为它是循环结束时的最终值。
当任何一个图片元素触发 onmouseover
事件时,它调用的是同一个事件处理函数,而这个函数内部引用的 index 变量已经是 5。
即所有图片元素触发 onmouseover
事件时都会执行 lightStars(5)。
因此,尽管我们希望每个图片元素触发 onmouseover
事件时都只执行对应的 lightStars
函数,但实际上它们都会执行 lightStars(5)
,因为它们共享了同一个闭包,这个闭包中的 ``index 已经被设置成了 5。
解决办法
通过在循环中使用 let 而不是 var 来声明 index 变量,循环的每次迭代都会有自己的 index 变量副本,这样事件处理程序中的闭包就能正确捕获。
for(let index=0;index<starImgs.length;index++){
starImgs[index].onmouseover=function(){
if(unsure){//判断是否为未确定评价状态
lightStars(index);//根据索引触发切换图片的函数
}
}
}
然后就能达到开头的效果实现五星好评了。
let和var的区别
说真的,看那么多let和var的区别还是没有自己去踩一次坑深刻😅
在JavaScript中,let 和 var 都用于声明变量,但它们之间有一些关键区别:
作用域:
var 声明的变量的作用域是函数作用域或全局作用域。如果在函数内部声明的变量,它只在该函数内部有效;如果在函数外部声明的变量,它则在全局范围内有效。
let 声明的变量的作用域是块级作用域。块级作用域通常由花括号({})定义,如 if 语句、for 循环、while 循环等。在块级作用域内部声明的变量只在该块内部有效。
变量提升:
使用 var 声明的变量存在变量提升(hoisting)现象,即变量可以在声明之前使用,但值为 undefined。这是因为在执行代码之前,JavaScript 会将 var 声明的变量提升到函数或全局作用域的顶部。
使用 let 声明的变量也存在变量提升,但它们不会被初始化为 undefined,而是保持在“暂时性死区”(Temporal Dead Zone,TDZ)中,直到执行到声明的位置才能被访问。
重复声明
使用 var 声明的变量可以被重复声明,而不会报错。
使用 let 声明的变量在同一作用域内不能被重复声明,否则会抛出 SyntaxError。
全局对象属性
使用 var 声明的全局变量会成为全局对象(window 或 global)的属性。
使用 let 声明的全局变量不会成为全局对象的属性。
总结
循环中,慎用var,用let还是稍微靠谱点