1 从一个尴尬的例子说起
<ul class="wrap">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var addEvent = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function () {
console.log(i)
}
}
}
var wrap = document.querySelectorAll('.wrap > li')
addEvent(wrap)
</script>
复制代码
- 结果如大家所想,点击每个
li
,都会打印3
addEvent
函数的本意,是想传递给每个事件处理函数:一个唯一的 i 。
而没有按照预期的原因是:事件处理函数绑定的是变量 i 本身,而不是函数在构造时的变量 i 的值。
2 解决
2.1 最常规的解决
利用的是对象属性不变性。
var addEvent = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].index = i
nodes[i].onclick = function () {
console.log(this.index)
}
}
}
复制代码
2.2 es6
利用的是
let
声明的变量,仅在块级作用域有效。
i 只在本轮循环有效。所以每次循环的 i 都是一个新的变量。JavaScript引擎内部会记住上一轮循环的值,作为初始化本轮变量 i 的基础。
而对于
var
,因为声明的变量是全局的,也就是说,使用的是同一个变量。最终在打印输出时,使用的就是全局变量 i ,所以点击时,都是 3。
let addEvent = function (nodes) {
for (let i = 0; i < nodes.length; i++) {
nodes[i].onclick = function () {
console.log(i)
}
}
}
复制代码
2.3 闭包(终于到了。。。)
如下所示,事件处理函数中,又返回了一个辅助函数。
- 因为闭包的特性,
- 该辅助函数,可以访问他被创建时所处的上下文环境(可以简单理解为:外部函数作用域。因为函数中声明的变量或是传递给函数的变量,是存放在执行上下文中,而执行上下文又挂靠在自己的作用域中。)
- 该辅助函数,访问的是外部函数中的实际变量,而不是复制后的值。
所以,每次传递给
handlers
函数的参数 i 都会放到handlers
的执行上下文中, 而每次调用函数,都会创建一个执行上下文对象,因为,互不干扰。
var addEvent = function (nodes) {
var handlers = function (i) {
return function () {
console.log(i)
}
}
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = handlers(i)
}
}
复制代码
3 其他问题
3.1 关于for
循环,使用let
和var
还有区别:
let
,设置循环变量的那部分,是父级作用域;而循环体内部是单独的子作用域
如下所示,会连续输出3个 a 。首先
let
是不能重复声明的,但该例中并没有报错。因为第一次循环结束,i++时,i 依旧是 0
for (let i = 0; i < 3; i++) {
let i = 'a'
console.log(i)
}
// a
// a
// a
复制代码
var
,因为通过var
声明的是全局变量,并且可以重复声明,所以当 i++ 时,变为了 a++, 则第二次循环时,i 为 NaN,就结束了循环。
for (var i = 0; i < 3; i++) {
var i = 'a'
console.log(i)
}
// a
复制代码
3.2 创建函数的问题
- 要避免在循环中创建函数。
回来最开始的地方,
为了给每个节点都绑定了事件处理函数,在循环中进行绑定。
这样,每次都会创建一个新的匿名函数,带来的只有无味的计算,还容易引起混淆,正如这个例子所示。
var addEvent = function (nodes) {
for (var i = 0; i < nodes.length; i++) {
nodes[i].onclick = function () {
console.log(i)
}
}
}
复制代码