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