循环一个节点列表(NodeList)或者数组,并且绑定事件处理函数引发对闭包的理解
循环一个节点列表(NodeList)或者数组,并且绑定事件处理函数引发对闭包的理解
当然可以使用事件委托来实现,而且性能更好,但是这里纯粹为了理解这个问题。
我在网上查阅了很多资料,有了一点理解,记录下来。如果有不对的地方,各位看客请指正。
代码如下:
HTML结构
<div id='one'></div> <div id='two'></div> <div id='three'></div>
JS处理 var odivs = document.getElementsByTagName('div'); for(var i=0; i<odivs.length; i++){ //遍历节点列表并且绑定事件 odivs[i].onclick = function(){ alert(i); } }
当我们点击任何一个DIV时,弹出的都是3;
这样会非常让人疑惑
那么我们换给写法。
var one = document.getElementById('one'); var two = document.getElementById('two'); var three = document.getElementById('three'); var i=0; one.onclick = function(){ alert(i); } i++; two.onclick = function(){ alert(i); } i++; three.onclick = function(){ alert(i); } i++ // i=3 大于2 退出循环
从这里知道,3这边值是i的最终的值。
然后我们再看事件处理函数。
function(){
alert(i);
}
从这里可以看出,这是一个函数声明,在面向对象中,这叫构造函数。
这只是一个函数声明,并且绑定到点击事件上,并没有被执行。然后代码继续往下,执行i++;
在网上看到这样一句话:
之所以是这样的,看过《JavaScript: The Definitive Guide》就会明白:
所有的JavaScript函数都是闭包:它们都是对象,都关联到作用域链。每次调用JavaScript函数的时候,都会为之创建一个新的对象来保存局部变量,并把这个对象添加至作用域链中(简单理解:作用域链就是对象列表)。
重点:"每次调用javascript函数时",局部变量会在函数被执行的时候创建,而不是声明的时候创建。
上面的例子,我们很直观的看出。三个函数中的i共享全局变量i;事件处理函数并没有被执行,代码接着往下走,
一直到脚本结束。 i的值变为3.
这时候,点击其中任意一个div,然后显示的是全局的i,i=3,所以得出了3这个值。
再往下看,可能有人会这么写,我就这么写过。
for(var i=0; i<odivs.length; i++){
odiv[i].onclick = function(){
return function(o){
alert(o);
}(i);
}
}
一看之下,好像没什么问题,变量i被一个内部函数保存了下来。函数也被执行了。
但是,函数真的被执行了吗?
分拆开来
odiv[i].onclick = function(){
return function(o){
alert(o);
}(i);
}
绑定了一个事件函数
function(){
return function(o){
alert(o);
}(i);
}
可以看出,并没有被执行。代码还是继续往下走。
并且代码也不会被执行。当时不知道怎么就写了这么个东西。
js 中只有 function 才会划分作用域(和 python 有点像),if/else、for 循环都不会划分作用域,所以原来的方式循环引用的都是同一个变量 i。
以上,我们可以得出一个结论:
要想把i的值保存下来。必须在执行过程中把i放到一个函数中,并且函数需要立即执行,这样0,1,2的值才会被保留下来。
那么修改代码
for(var i=0; i<odivs.length; i++){
odivs[i].onclick = show(i);
}
function show(i){
alert(i);
}
那么,这样写行吗。显然是不行的。
虽然在绑定事件的时候,调用了一个外部函数,并且将i传递进去保存了下来。
但是,结果会是没循环一次,调用一次show函数。然后弹出一个数字。并没有在onclick并没有绑定事件处理函数,因为show函数并没有返回值,或者返回值为undefined ,所以上面的代码应该是这样的
for(var i=0; i<odivs.length; i++){
show(i);
odivs[i].onclick = undefined;
}
好吧,以上是我在学习过程中犯的一些错误,和自己慢慢深入后的一些理解。下面正式来说说解决这个问题的办法。
上面说过:
1.要想把i的值保存下来。必须在执行过程中把i放到一个函数中,并且函数需要立即执行。
2.函数的返回值应该是一个事件处理函数。否则事件绑定就没有意义了。
修改后代码如下。
for(var i=0; i<odivs.length; i++){ odivs[i].onclick = show(i); // 讲变量i传入函数立即执行,保留下来。 } function show(i){ return function(){ //返回一个事件处理函数。函数内部调用alert的方法将i显示出来。 alert(i); } }
//结果 0,1,2 正确。
然后我们写在一起
通过一个佚名函数实现,道理和上面一样。
for(var i=0; i<odivs.length; i++){ odivs[i].onclick = (function(i){ return function(){ alert(i); } })(i); }
当然还可以这么写
先把i保留下来,再绑定事件。顺序反一下。
for(var i=0; i<odivs.length; i++){ (function(i){ odivs[i].onclick = function(){ alert(i); } })(i); }
至此,这个问题处理完毕。
文章中很多来自 http://www.zhihu.com/question/20019257 ,感谢!
百度搜了很多这个问题,排前面的都是几个li,然后出现这个问题,然后就是一个解决办法,而且大多数网站都是这样,抄来抄去,连标点都一样,很让人困惑,浪费事件。
上面这篇内容对我启发很大。讲讲自己的理解。
我来讲讲自己的理解。
首先说了,JS其实也分传值和传地址 也就是 by value and by sharing. 对应java,C等语言的 by value 和 by reference
var bar;
var foo = bar;
bar = {'key' : 'value'};
console.log(foo , bar );
这样一个案例。
输出的是 undefined 和 {'key':value};
从这里可以看出,foo=bar 是把 bar的值传递给了 foo ; 因为一开始 bar没有赋值,值是undefined,所以foo也是undefined,bar再赋值。
假如foo=bar 是将 bar的引用传递给了 foo的话,那么 修改bar的值, foo输出的也应该是修改后的值。
再看如下代码
var d = new Date();
console.log(d.getMinutes());
var a = d;
a.setMinutes(18);
console.log(d.getMinutes());
// 18
测试的时候是 48 18
很明显, a = d 时,是将 引用传递给了 a ,修改a的属性时,d也跟着变了。
看JS中的作用域(scope) 看原文,已经写的不错了。
自 http://www.zhihu.com/question/20019257
个人的一点心得,还在学习中,如有不对的地方,还望指出。