-
Notifications
You must be signed in to change notification settings - Fork 17
Description
一个场景
假设现在有这么一个场景,在一个<ul/>
里面有10个<li/>
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
然后要求每一个<li/>
点击之后都会填入该<li/>
的index
。
看起来是十分简单的需求,很多经验不足的人里面就洋洋洒洒写下一段(比如以前的我):
const li = document.querySelectorAll('ul>li');
for(var i = 0; i < li.length; i++) {
li[i].addEventListener('click', function(e) {
this.innerHTML = i;
});
}
这段看起来没有任何问题,按照预期,会是这样子(每一个li被点击后):
但是的代码运行起来却是这样子(每一个li被点击后):
这就奇怪了,为什么明明写明了每次click
都会将当前的i赋值给当前li的innerHTML
,为什么会出现10个10呢?
函数与作用域
javascript与其他语言不同,js没有块级作用域,而只有函数作用域,也就是说,js中,在for
或if
等代码块中定义的变量都会默认变成全局变量(window
对象下的一个属性):
if(exp) {
var x = 20;
//这里的x是全局变量
}
for(var i = 0; i < n; i++) {
//这里的i是全局变量
}
这意味着什么呢?作用域的作用是用作隔离代码块外部对代码块内部的影响,使作用域内部的变量独立于作用域外部的变量,也就是说作用域有着锁定变量的功能。所以这意味着在上面代码中的任何一处地方更改i
或x
的值,都会对if
和for
内部产生影响。
而函数的作用域要在函数运行的那一刻才会产生,函数表达式并没有作用域。
我们再来看刚刚开始的例子。
现在很明显知道,循环变量i
也是一个全局变量,在addEventListener
中的回调表达式并没有产生独立的作用域,所以很明显就能想到,functiion
里面的i
随着循环的自增一直在变化,当用户点击<li/>
时,addEventListener
中的function
才真正执行,产生函数作用域,但是这时的i
早已变成了10。
for(var i = 0; i < li.length; i++) {
//i是全局变量,不断地++
li[i].addEventListener('click', function(e) {
//这里面的i是全局的i,也一直在变化
this.innerHTML = i;
});
}
原来如此,看来《javascript: the good part》这么薄也不是没有道理的。
解决办法
既然已经知道了原因,那么解决的办法也应该很容易想到。
目前主流的办法有三种:
第一种:使用ES6let
关键字
let
关键字修复了js没有块级作用域的问题,我们用let
代替var
改写原始代码:
const li = document.querySelectorAll('ul>li');
//使用let
for(let i = 0; i < li.length; i++) {
li[i].addEventListener('click', function(e) {
this.innerHTML = i;
});
}
用let
应该是最简单的方法,但是let
的兼容性还比较差(至少我知道微信浏览器还不兼容),所以应用的时候尽量慎重。
第二种,用IIFE
IIFE是立即执行函数表达式(Immediately Implement Function Expression)的简称,使用IIFE我们可以让一段函数表达式马上执行。
使用IIFE改写原始代码:
const li = document.querySelectorAll('ul>li');
for(var i = 0; i < li.length; i++) {
/*使用IIFE,让马上执行一段函数表达式,每次循环都会生成一个新的函数作用域,将变量i的当前值锁定在IIFE的作用域里面,作用域里面的i不会受到外面的i的自增的影响
/*同时返回一个闭包,这个闭包可以访问到保存后的i的值
/*可能有点绕,但是就是这个道理
*/
li[i].addEventListener('click', (function(i) {
return function(e) {
this.innerHTML = i;
}
})(i));
}
这种方法用得比较多,因为兼容性是最好的。但是要理解起来会花点时间,特别是对闭包和作用域不了解的朋友。
第三种:我觉得是最优雅的方法,使用map
为什么我会觉得这种办法是最优雅呢,因为他很有functional programing
的味道:
[].slice.call(document.querySelectorAll('ul>li')).map((item, index) =>
item.addEventListener('click', e => {
item.innerHTML = index;
})
);
真的很美,流畅简洁直观的美。
其实这种方法跟第二种方法的本质是一样的,都是在循环的时候执行一个函数来产生函数作用域(第二种是用执行IIFE,这个是执行作为参数传入map
方法的函数,循环变量就是index
)。
---EOF---
Activity