美文网首页
关于js中for循环里闭包的一些思考

关于js中for循环里闭包的一些思考

作者: 把天聊死了 | 来源:发表于2017-03-22 21:05 被阅读0次

    在文章的开头,简单来提一下,什么是闭包。

    JavaScript中的闭包,就像一个副本,将某函数在退出时候的所有局部变量复制保存其中。 这些函数可以“记忆”他被创建时候的上下文环境,将外部变量保留在栈帧中。

    首先,我们来看这样一个例子:

    function buildList(list) {
        var result = [];
        for (var i = 0; i < list.length; i++) {
            var item = 'item' + i;
            result.push( function() {console.log(item + ' ' + list[i])} );
        }
        return result;
    }
    function testList() {
        var fnlist = buildList([1,2,3]);
        // 使用j是为了防止搞混---可以使用i
        for (var j = 0; j < fnlist.length; j++) {
            fnlist[j]();
        }
    }
     testList() //输出 "item2 undefined" 3 次
    

    在上面这个例子中,console会输出三次“item2 undefined”

    01.png
    为什么没有按照for循环的顺序,输出 item0item1item2的值呢?
    这是因为在上述代码中,for循环里产生了一个闭包,当result.push( function() {console.log(item + ' ' + list[i])} );这行代码运行时,由于i变量并没有在这个无名函数中定义,所以会到上层语义环境中去找。此时,for循环并不会因此中断,等待无名函数。那么当无名函数在上层语义环境中找到i的值,这是for循环已经结束,i的值自然变成了3,而item此时的值则为item2.
    那么在接下来的for循环中
    function testList() {
        var fnlist = buildList([1,2,3]);
        // 使用j是为了防止搞混---可以使用i
        for (var j = 0; j < fnlist.length; j++) {
            fnlist[j]();
        }
    }
    

    实际上时运行了三次console.log('item2' + ' ' + list[3]),由于传入函数bulidList的数组为[1, 2, 3]list[3]的值类型自然是undefined
    若是我们将传入的数组做出一点修改,就会发现,console会将list[3]的值正确输出。第一个for循环中的判断条件由i < list.length改为i < 3,传入的数组由[1, 2, 3]改为[1, 2, 3, 4]

    function buildList(list) {
        var result = [];
        for (var i = 0; i < 3; i++) {
            var item = 'item' + i;
            result.push( function() {console.log(item + ' ' + list[i])} );
        }
        return result;
    }
    function testList() {
        var fnlist = buildList([1, 2, 3, 4]);
        // 使用j是为了防止搞混---可以使用i
        for (var j = 0; j < fnlist.length; j++) {
            fnlist[j]();
        }
    }
     testList() //输出 "item2 4" 3 次
    
    02.png

    如果在for循环中产生了闭包,我们如何让它输出我们想要的结果呢?再来看一个例子吧。

    var list = document.getElementById("list");
    
     //插入五个<li>标签
    for ( i = 1; i <= 5; i++) {
      var item = document.createElement("LI");
      item.appendChild(document.createTextNode("Item " + i)); 
    
    //分别为五个<li>标签绑定onclick事件
      item.onclick = function (ev) {
        console.log("Item " + i + " is clicked.");
      };
      list.appendChild(item);
    }
    

    这个例子中,我们想要的效果是点击不同的<li>标签,console会输出对应的Itemi is cilick。但是由于for循环里面产生了闭包,实际的结果是无论点击哪个<li>,console输出的都是Item 6 is clicked.

    03.png

    如果我们将代码稍作修改,再增加一层闭包,并将i作为参数传入到函数中,我们将会得到正确地输出。

    for ( i = 1; i <= 5; i++) {
      var item = document.createElement("LI");
      item.appendChild(document.createTextNode("Item " + i));
      (function(i){
      item.onclick = function (ev) {
        console.log("Item " + i + " is clicked.");
      };
      list.appendChild(item);
      })(i);
    }
    
    04.png
    这段代码中,虽然console.log("Item " + i + " is clicked.");仍然需要去上层语义环境中找i的值,但是由于外面增加了一个function,并将i作为参数传入,此时便可以寻找到正确地值。在这里,每一次for循环,都会产生一个大的闭包,实际上到循环结束,共产生了五个闭包,这五个闭包里面分别存储了i从1-5的五个值。如果我们不将i作为参数传入会是什么样的?
    for ( i = 1; i <= 5; i++) {
      var item = document.createElement("LI");
      item.appendChild(document.createTextNode("Item " + i));
      (function(){
      item.onclick = function (ev) {
        console.log("Item " + i + " is clicked.");
      };
      list.appendChild(item);
      })();
    }
    
    05.png
    可以看到,跟之前没有在外层套上函数时是一样的输出。那么有没有什么办法不传i作为参数也可以得到正确的输出呢?有的!看下面的代码。
    for ( i = 1; i <= 5; i++) {
      var item = document.createElement("LI");
      item.appendChild(document.createTextNode("Item " + i));
      (function(){
        var j = i;
        item.onclick = function (ev) {
            console.log("Item " + j + " is clicked.");
        };
        list.appendChild(item);
        })();
    }
    

    这段代码中,我增加了var j = i;,并将之前的i改为j。此时,已经能够得到正确的输出。

    06.png

    为什么增加了一个var j = i就可以得到正确的输出了呢?实际上原理和上面并没有变化,主要是因为这里产生了五个闭包,每一个闭包里面的j都引用了一个i值。但再稍加修改,就又会不同。接下来,我将var j = i;改为j = i;,看看会有什么变化。

    07.png
    这里的输出又出错了,会得到五个同样的输出。但是请注意了虽然同是同样的输出,却与之前略有不同。这里的五个输出都是Item 5 is clicked.而之前则是Item 6 is clicked.
    得到错误的输出是因为去掉了关键字var之后,j的作用域发生了变化,成为了全局变量。五个j引用了同一个i值。至于为什么是5而不是6,则是因为j引用的是最后一次循环时的i值,而不是循环结束以后的i值。
    最后,这篇文章中的内容,是我在看ES6标准中let关键字相关的内容时想到的。那么你肯定会问了,是不是 let也可以解决for循环中闭包的问题?Bingo!再来看看下面的代码吧。
    for ( i = 1; i <= 5; i++) {
      var item = document.createElement("LI");
      item.appendChild(document.createTextNode("Item " + i));
      let j = i;
      item.onclick = function (ev) {
            console.log("Item " + j + " is clicked.");
        };
      list.appendChild(item);
    }
    

    这里let创建的变量j是拥有块级作用域的,在ES6之前js是没有块级作用域的。
    当然,解决办法还有很多,你觉得哪种办法最优雅呢?

    相关文章

      网友评论

          本文标题:关于js中for循环里闭包的一些思考

          本文链接:https://www.haomeiwen.com/subject/lkgonttx.html