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


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

    Oliver发布于 2019-09-16

    由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语法的甜头,以后应多使用新标准和新技术。
    • 反思:不该在糟粕的地方太过于钻牛角尖,避免浪费时间。
    阅读 1.3k更新于 2019-09-24
     
  • 相关阅读:
    flask点滴
    CMD批量处理
    pymssql中文乱码
    vb cllection
    更改用户环境变量
    解开未完成的事务,用变量接收另一个存储过程反回的值
    gitlab-ci一些笔记
    Linux系统查看cache/buffer占用比较大的进程
    kubeadm证书过期解决方案
    ceph12版本部署实践
  • 原文地址:https://www.cnblogs.com/sexintercourse/p/16304486.html
Copyright © 2020-2023  润新知