在这个点击事件函数中,为了记录点击的参数按钮的参数是多少定义index变量并赋值,但是为什么这句话一定要放在外面的函数才生效,而放在点击函数里面却会出现报错
先上一张正确的图
下面这张是错误的图
在这个点击事件函数中,为了记录点击的参数按钮的参数是多少定义index变量并赋值,但是为什么这句话一定要放在外面的函数才生效,而放在点击函数里面却会出现报错
先上一张正确的图
下面这张是错误的图
主要涉及知识点有:
在函数区域内,如果有声明变量,则使用内部的变量,如果没有声明该变量,它会一直沿着作用域向上找,找不到时,则使用函数外声明的全局变量。
函数中有变量声明,会提前到函数体的最前面。
因此:
图2:因为for循环中声明了i变量,因此变量提前至函数体最前面,因为没有初始化因此值为undefined;
图1:i在外面赋值因此取得的是外围函数i值通过循环赋值。
上图吧:
代码:
点击之后输出:
先看第二种情况:
赋值在click的匿名函数里面,这个是循环结束之后,当你点击的时候才发生,此时内存里面的i已经执行到了length
,所以你再点击的时候就是数字length
,
再看第一种情况:
赋值在匿名函数之上,每一次循环的时候会给index
赋值i
,当你点击的时候,此时没有再设置变量,所以你点哪个,就是你最初想要的效果,
如何解决:
1.按照你的第一种写法可以的
2.
oLi[i].click = (function(i){
//把i分配进去
})(i)
3.使用ES6的let试一下
看第二张图,onclick = function
只是绑定事件,函数并未执行,在你触发点击事件的时候执行,这时执行oLi[i].index
中的i是什么值,此时它是等于oLi.length
的。
的确涉及到了一个比较隐蔽的闭包,但从需求上来说这个闭包是不应该像这样存在的。
首先JS是事件驱动,在给onclick事件赋值的过程中只是将函数体放到了一块内存区域中(匿名函数都是如此,除非用立即执行的方式进行书写),当click事件触发时才回调用这个事件,因此你在这个循环内声明的所有onclick事件在声明时都不会立即引用i的值,只有在每次执行click事件的时候会将这个值引用,这就形成了闭包。但循环体在脚本加载完以后就已经执行完毕,这个时候i已经等于了oLi.length值,再进行引用的时候所有的结果都是最后一个按钮的索引值。
在这种类型的需求中,应当将事件的声明封装成一个方法,在循环体中直接执行这个方法,才能捕捉到这个i的值。
function binde( index, obj ) {
obj.index = index;//将索引值传递给要绑定事件的对象
obj.onclick = function() {
console.log( this.index );
}
}
//循环体
for ( var i = 0; i < oLi.length; i ++ ) {
binde( i, oLi[i] );//这个时候将i赋值到binde中,并且立即执行,这个时候i的值就被binde的形参index捕捉了
}
PS:其实在这里还是形成了闭包,在binde执行完以后onclick依然引用这个函数里的index,但逻辑上来说每个事件也应当有属于自己闭包来一一对应。
首先要纠正下 “为什么这句话一定要放在外面的函数才生效,而放在点击函数里面却会出现报错” 这句话, 其实放在里面也是可以的,但是你写的方法不对。
这个是我根据题主写的一段测试代码:
var oLi = $('li');
for (var i = 0; i < oLi.length; i++) {
oLi.eq(i).on('click',function() { console.log('click li');
oLi[i].index = i;
for (var i = 0; i < oLi.length; i++) {
oLi[i].className = i;
}
})
}
直接在 segmantfault 运行会报错,错误提示 VM722:5 Uncaught TypeError: Cannot set property 'index' of undefined(…)
这个错误不是由于闭包问题引起的,而是由于 变量提升 导致的,我们知道在 JS 中只有函数才有作用域,而在作用域中, 由于 JS 有变量提升机制,用 var 定义的变量会被提前到作用域的顶部声明, 但是赋值还是在原来的位置赋值,所以上面的代码在执行的时候实际上会变成这样:
var oLi = $('li');
for (var i = 0; i < oLi.length; i++) {
oLi.eq(i).on('click',function() { console.log('click li');
var i; // i 声明了,但是未定义
oLi[i].index = i; // 这里 i === undefined,报错
for (i = 0; i < oLi.length; i++) {
oLi[i].className = i;
}
})
}
所以如果想使用外部的 i, 那么函数里面不能声明另一个变量 i ,否则函数是不会循着作用域链去获取外部的变量 i的,这个时候,你就知道得将函数里面的循环 i 变量名改为其他名称如 j:
var oLi = $('li');
for (var i = 0; i < oLi.length; i++) {
oLi.eq(i).on('click',function() { console.log('click li', i);
oLi[i].index = i;
for (var j = 0; j < oLi.length; j++) {
oLi[j].className = j;
}
})
}
这个时候就不报错了吧?但是你会发现,点击oLi的某个元素依旧报错,看打印出来的 i值, 你会发现值为 oLi的长度,这个时候就是闭包的问题了,闭包的意思你可以理解为可以获取外部对象的值,也就是说只要闭包没有结束,外部被引用的对象不会被销毁,但是你要记住,闭包引用的是变量的整个对象,而不是某个阶段的值,所以当使用闭包的时候,你获取到的永远是执行的时候引用对象最后的值,这个时候你就明白为什么 i的值为 oLi.length, 因为闭包的执行是在循环完成之后,所以当执行到 oLi[i].index = i , 此时 i 的值为 oLi.length, 所以边界溢出,报错。
解决的方法就是把i作为一个参数传入并即时执行:
var oLi = $('li');
for (var i = 0; i < oLi.length; i++) {
(function(i){
oLi.eq(i).on('click',function() { console.log('click li', i);
oLi[i].index = i;
for (var j = 0; j < oLi.length; j++) {
oLi[j].className = j;
}
})
})(i)
}
这个时候 i不再是作为外部引用的变量,而是作为参数传入, 让我们能正常拿到想要的 i 值。
10 回答11.1k 阅读
6 回答3k 阅读
5 回答4.8k 阅读✓ 已解决
4 回答3k 阅读✓ 已解决
2 回答2.6k 阅读✓ 已解决
3 回答5.1k 阅读✓ 已解决
5 回答1.9k 阅读
的确是函数闭包问题。
第一种