前端面试之闭包的应用详解(含易错点讲解)
应用一:点击li输出当前li的索引号
<ul class="nav"> <li>星辰</li> <li>大海</li> <li>春暖</li> <li>花开</li> </ul>
步骤:
》》第1步:获取所有的小li
// 获取所有小li var lis = document.querySelector('.nav').querySelectorAll('li');
》》第2步:循环绑定事件(2种方式)
方式①----利用动态添加属性的方式
//循环绑定点击事件--利用动态添加属性的方式 for (var i = 0; i < lis.length; i++) { lis[i].index = i; lis[i].onclick = function() { console.log(this.index);//this指向当前被点击的小li } }
【解析】
- 通过循环给每个小li动态添加一个属性index,其值为当前li的索引号
- 打印输出当前被点击小li的index属性
注意:此处不能直接console.log(i)
for (var i = 0; i < lis.length; i++) { lis[i].onclick = function() { console.log(i); } }
点击每个小li输出结果将为:
原因:因为function()是一个异步任务,在点击了小li之后才会执行,而for循环为同步任务会立马执行,故点击任何小li输出结果都为4。这也是为什么需要专门添加一个index属性原因
方式②-----利用闭包的方式得到当前小li的索引号(通过立即执行函数实现点击小li输出索引号,闭包为立即执行函数)
//循环绑定点击事件--利用立即执行函数(闭包的应用) for (var i = 0; i < lis.length; i++) { (function(i) { lis[i].onclick = function() { console.log(i); } })(i); }
【解析】
- 利用for循环创建了4个立即执行函数
- 立即执行函数最后一个小括号()相当于是调用该函数,故其可以写入需要被传递给形参的实参。此处实参写入循环变量i的值,function(i)中的形参i用来接受被传递过来的实参i。由于每一次循环都会创建一个立即执行函数,在第一次创建的时候将循环变量i=0传递给实参i,再将实参i=0传递给形参i,当前被创建的立即执行函数中就存储i的值为0,在第二次创建的时候将i=1传递给实参i,再将实参i=1传递给形参i,当前被创建的立即执行函数中就存储当前i的值为1,……循环结束后被依次创建的4个立即执行函数存储的i的值分别为0、1、2、3
- 将需要循环绑定的点击事件函数写入立即执行函数中,当点击了第0个小li时打印出0,当点击了第1个小li时打印出1……,点击事件函数所利用的i值是另外一个函数即立即执行函数里面的i值,因为一个函数使用了另外一个函数里面的变量,故此时就会产生闭包。
【小结】
此处显然方式①比方式②更加简单一点,方式②每次循环都会创建一个立即执行函数,要创建好多好多次反而更麻烦了,所以说并不是所有闭包都是好的,在某些情况下效率反而更低一点。按道理来说,当立即执行函数执行结束后i应该销毁,但立即执行函数存在点击事件需要用到i,故必须等待点击事件执行结束后变量i才可以被销毁,一直不点击那么i一直不能被销毁,这也称为内存泄漏(变量本来需要被销毁但一直未被销毁)。
如果在某些情况下,需要延长变量的作用范围就可使用闭包,在实际开发中需根据实际情况来合理选择是否使用闭包
应用二:3秒之后动态打印所有li元素的内容
<ul class="nav"> <li>星辰</li> <li>大海</li> <li>春暖</li> <li>花开</li> </ul>
步骤:
》》第1步:获取所有的小li
// 获取所有小li
var lis = document.querySelector('.nav').querySelectorAll('li');
》》第2步:循环绑定定时器函数
for (var i = 0; i < lis.length; i++) { (function(i) { window.setTimeout(function() { console.log(lis[i].innerHTML); }, 3000) })(i); }
【解析】
利用小闭包立即执行函数来做,将定时器函数写入立即执行函数的里面,每次创建立即执行函数的时将当前的循环变量传入立即执行函数,时间一到定时器函数打印的i值即为其所处立即函数中i的值,一个函数使用了另外一个函数中的变量故 产生了闭包
注意:如果不使用立即执行函数将报错
for (var i = 0; i < lis.length; i++) {
//去掉立即执行函数 window.setTimeout(function() { console.log(lis[i].innerHTML); }, 3000) }
3秒后的打印的结果:
原因:定时器里面的回调函数属于异步任务,异步任务首先会被放入任务队列中,只有被触发了才会执行,而for循环属于同步任务会立马执行将i的值会变成4,此时时间一到再去调用回调函数打印的将为lis[4],由于不含有lis[4]故将报错。
应用二:计算打车价格
var car = (function() { var start = 13; //起步价 var total = 0; //总价 return { price: function(n) {}, //返回正常价格 n为公里数 yd: function(flag) {} //返回拥堵后的费用 flag判断是否拥堵 } })()
//在立即函数中,不需要调用立即执行并return返回了包含两个函数的对象,此处利用car来接收该对象,利用car.price()和car.yd()来调用
var car = (function() { var start = 13; //起步价 var total = 0; //总价 return { price: function(n) { if (n <= 3) { total = start; return total; } else { total = start + (n - 3) * 5; return total; } }, //返回正常价格 n为公里数 yd: function(flag) { return flag ? total + 10 : total; } //返回拥堵后的费用 flag判断是否拥堵 } })();
start和total在位均为局部变量,被提供给了另外两个函数price和yd使用故产生了闭包。此处立即执行函数为一个闭包,start和total均为闭包变量。