4

由for循环引发的关于var和作用域的思考

Stage 1

  • 起因是在某技术博客里看到了如下代码
function fun(){
    for(var i=0; i<lis.length; i++){    //此处的length=5
      lis[i].onclick = function(){
        console.log(i);
      }
    }
}

于是我在console里写入了如上代码,依次点击lis,输出了五次4,这对于写惯了c语言的我是一个观念上的颠覆,于是开始了大规模的资料查找,试图解决我的这个疑惑。

Stage 2

  • 在经过几番询问和一些技术博客的翻阅之后,得到了如下的一种解释:

"在这个函数里面的i其实引用的是最后一次i的值,为什么不是1,2,3,4...呢? 因为for循环中并没有执行这个函数,这个函数是在你点击的时候才执行的,当执行这个函数的时候,它发现它自己没有这个变量i,于是向它的作用域链中查找这个变量i,因为当你单击这个box的时候已经for循环完了,所以找到的i是最后一次赋值后的i"

  • 本以为事情到此结束了,可我感觉还是差了些什么,下面这篇博客解开了我心中的最别扭的结。

引用自:https://www.cnblogs.com/qiegu...

function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}
var funcs = createFunctions();
for (var i=0; i < funcs.length; i++){
    console.log(funcs[i]());
}

陷阱就是:函数带()才是执行函数! 单纯的一句 var f = function() { alert('Hi'); }; 是不会弹窗的,后面接一句 f(); 才会执行函数内部的代码。上面代码翻译一下就是:

var result = new Array(), i;
result[0] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
result[1] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
...
result[9] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换!
i = 10;
funcs = result;
result = null;

console.log(i); // funcs[0]()就是执行 return i 语句,就是返回10
console.log(i); // funcs[1]()就是执行 return i 语句,就是返回10
...
console.log(i); // funcs[9]()就是执行 return i 语句,就是返回10

"为什么只垃圾回收了 result,但却不收了 i 呢? 因为 i 还在被 function 引用着啊。好比一个餐厅,盘子总是有限的,所以服务员会去巡台回收空盘子,但还装着菜的盘子他怎么敢收? 当然,你自己手动倒掉了盘子里面的菜(=null),那盘子就会被收走了,这就是所谓的内存回收机制。"

Stage 3

  • 在《JavaScript高级程序设计》的7.2节终于巩固了我的理解:

“作用域链的机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。”

“表面上看,每个函数都应该返回自己对应的i值,但实际上每个函数都返回了一样的值。因为每个函数的作用域链中都保存着fun()函数的活动对象,所以他们引用的都是同一个变量i。当fun()函数返回后,变量的i值是4,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10。”

  • 想法:绑定的函数并不是立刻就实现,而是处于等待调用的状态。当程序的执行流进入一个函数的时候,这个函数被推入一个环境栈中,再进行变量读取和函数内容的实现。
  • 实例化地,在本篇开头的代码中,五次循环将lis[i].onclick事件分别绑定在了五个匿名函数上,开辟了五个执行环境,进而形成了五条作用域链,形如:[闭包]→[fun()的活动对象]→[全局变量对象],而很容易理解地,fun()活动对象是这五条作用域链所共享的,自然i值也就是共享的了

Stage 4

  • 这部分该讲讲解决方法了
  • 高程上推荐的方法:通过创建另一个匿名函数强制让闭包行为符合预期
function fun(){
    for(var i=0; i<lis.length; i++){    //此处的length=5
      lis[i].onclick = (function(num){
          return function(){
            console.log(num);
          }
      })(i)
    }
}

这种方法在每次循环中,用立即执行的匿名函数记录下了当前的i值(num),并创建了单独的作用域,又在匿名函数中创建了一个新的闭包,接收i(num)值,形成了单独的作用域链。

  • es6中let方法:
function fun(){
    for(let i=0; i<lis.length; i++){    //此处的length=5
        lis[i].onclick = function(){
            console.log(i);
        }
    }
}

虽然本人还没有正式开始es6的学习(捂脸,但因涉及本篇博客的解决方法,还是认真地了解了一下let关键字。此方法的成功,最大的功臣便是let的块级作用域特点,他在每次循环中生成了单独的作用域,达到了与上一种方法相同的效果。

Stage 5

研究这个看似很简单的特性耗费了整整一天的时间,也深深体会到了为什么说JS语言的糟粕不少。

  • 总结:

    • for循环体内定义函数 ,若函数体内用了for块内的var变量,在for语句外调用该函数时,该函数采用的是循环结束后的var值
    • 而块内用let变量,与之同级的函数体用了该let变量,之后调用函数,函数使用的是定义时块内的let变量值。
  • 收货:更加明确了关于作用域、闭包等概念。尝到了ES6语法的甜头,以后应多使用新标准和新技术。
  • 反思:不该在糟粕的地方太过于钻牛角尖,避免浪费时间。

Oliver
76 声望13 粉丝

Slow Done, Achieve More.


« 上一篇
HTML常用标签
下一篇 »
css3 特性记录