for循环经典闭包问题的衍生题

    <ul>
        <li>0</li>
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
var nodeList = document.getElementsByTagName('li');

for(var i = 0;i < nodeList.length;i++){        
    nodeList[i].onclick = function(){            
        console.log(i);
    }
}
这里每个li的onclick触发后 都会输出4 ,因为console.log(i)只是对i变量的引用

如果改成一下代码:
for(var i = 0;i < nodeList.length;i++){
    nodeList[i].num = i;
    nodeList[i].onclick = function(){            
        console.log(this.num);
    }
}

可以正常输出0,1,2,3  猜测原因是在为li创建onclick事件的同时创建属性num
所以num和onclick都是属于li的一部分,那么再变化一下代码

for(var i = 0;i < nodeList.length;i++){
    var num = i;
    nodeList[i].onclick = function(){            
        console.log(num);
    }
}

这里依然会输出4,如果说是因为引用了num,而num又引用了i 
那么,为什么以nodeList[i].num = i 的可行? 
为li创建了属性num的同时 
nodeList[i].num = i 算是对i的引用还是赋值?
阅读 2.9k
2 个回答

你这样的学习方法真的太可怕啦!.

闭包会都输出4的原因是, 函数定义时并不执行.

var nodeList = document.getElementsByTagName('li');

for(var i = 0;i < nodeList.length;i++){        
    nodeList[i].onclick = function(){            
        console.log(i);
    }
}

之所以输出的全部是3, 是因为li绑定的事件处理函数在定义时并未执行, 当你触发li的点击事件时函数执行, 此时的i是3,当然会输出3.

for(var i = 0;i < nodeList.length;i++){
    nodeList[i].num = i;
    nodeList[i].onclick = function(){            
        console.log(this.num);
    }
}

为li增加一个属性, 为num属性赋值是在什么时候发生的? 他并没有放到函数里, 所以这个语句直接就执行啦. 此时i就分别为0,1,2,3, 等事件处理函数执行的时候, 输出的是对应li的num值, 而这个时候的num已经在上一步就为其赋值啦. 当然会输出0,1,2,3

for(var i = 0;i < nodeList.length;i++){
    var num = i;
    nodeList[i].onclick = function(){            
        console.log(num);
    }
}

这个是因为, num变量被覆盖了值.
假设代码改成这样:

var num;
for(var i = 0;i < nodeList.length;i++){
    if(i === 1) {
        num = i;
    }
    nodeList[i].onclick = function(){            
        console.log(num);
    }
}

你说他会输出什么?

我说的学习方法可怕, 是你不好好看书, 理解他的真义. 就这样钻这些牛角尖. 是会出事儿的.

有趣的问题,我来回答参考一下。

var nodeList = document.getElementsByTagName('li');

for(var i = 0;i < nodeList.length;i++){        
    nodeList[i].onclick = function(){            
        console.log(i);
    }
}

第一个代码一定会是4,原因是console.log在执行时,并不会记住在过程中的i值。
onclick后面指的是个回调函式,所以相等于下面更明确的改写:

var nodeList = document.getElementsByTagName('li');

var i; //独立出来

function output(){  //独立出来          
   console.log(i); 
}

for(i = 0; i < nodeList.length; i++){        
    nodeList[i].onclick = output;
}

为何要把var i;独立出来,因为在这里for中所宣告的i,相当于在全域中宣告。另一个console.log也是,它是个回调函式,没执行(没按下去触发)不会呼叫的。所以当console.log真正被执行时,这个时候i老早就经过跳出for语句,变成了4。


第二例是这样:

for(var i = 0;i < nodeList.length;i++){
    nodeList[i].num = i;
    nodeList[i].onclick = function(){            
        console.log(this.num);
    }
}

这回改用nodeList[i].num属性先接好每次for运算的i值,相当有用,然后也知道要用this值传给console.log作输出。

我假设你知道了几件事:

  1. 你知道getElementsByTagName的回传是什么,也就是如上面所看起来你用了像阵列的用法这样。

  2. nodeList[i].num这样的用法,是在物件中新增属性

  3. console.log(this.num),这边的this是代表什么意思,对象是谁

你的猜测是正确的: 猜测原因是在为li创建onclick事件的同时创建属性num,所以num和onclick都是属于li的一部分。


来看第三个例,最后再说明我的看法。

for(var i = 0;i < nodeList.length;i++){
    var num = i;
    nodeList[i].onclick = function(){            
        console.log(num);
    }
}

在第三例中,你会认为var num的效果会和第二例的nodeList[i].numthis.num的用法一致,所以这样用。实际上并没有,它与和第一例是一样的。

所以我也猜测你可能在第二例中,不知道在作什么,所以我以下要说明一下。

  • 什么时候是引用,什么时候是赋值?

num = i;var i = 0这个叫赋值,nodeList[i].num = i;这个也叫赋值,JavaScript基本上看到等号(=)的都是值传递。只有几种情况会看到所谓的"引用"(参照),第二例有个,就是this引用(指向)到元素物件。

然后console.log()只是个函式,你当下给他什么值作为传参,他就输出在控制台中,就这样,没什么引不引用。

  • 你用了很多不好的语法

第一个是,nodeList[i].num = i;就是个,nodeList[i]的确是个物件,但原先没有的东西你生了一个给他,这个作法不是很好,也没见人这样用的。元素(Element)的话可以加上data-开头之类的属性,有API可套用,不过这是HTML5的标准。你这里应该是NodeList与Node物件,详细要看API说明。

第二个是你在for语句里宣告var num = i;,for语句里不就执行4次就宣告4次?我当然知道js引擎都会优化,所以这是个不好的语法。要不就写到for(var i = 0, num = 0;...,要不然就先在for语句外面宣告。

  • 接著我要说明如何改进你的代码,让它可以达到你想要的,以下是几个范例:

第一种: 用bind产生新函式,然后指定给onclick:

for(var i = 0;i < nodeList.length;i++){    
    nodeList[i].onclick = function(a){            
            console.log(a)
    }.bind(null, i)
}

第二种:用IIFE,然后回传新函式:

for(var i = 0;i < nodeList.length;i++){
    nodeList[i].onclick = (function(a){
      return function(){
        console.log(a)
      }
    })(i)
}

上面两例用的技术类似,其实都先锁住console.log只能输出什么了。差不多这样,搞得太复杂可能又有太多疑问。

推荐问题