作用域与闭包

作者: tency小七 | 来源:发表于2018-05-26 11:17 被阅读0次

    http://www.cnblogs.com/wangfupeng1988/p/3986420.html

    执行上下文

    什么是执行上下文(也叫做“执行上下文环境”)?暂且不下定义,先看一段代码:

    console.log(a) //ReferenceError: a is not defined
    
    console.log(a) //undefined
    var a
    
    console.log(a) //undefined
    var a = 100
    

    第一句报错,a未定义,很正常。第二句、第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道了a是undefined,但却不知道a是10(第三句中)。

    在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些准备工作,也就是我们所说的变量提升。

    不仅如此,需要注意代码注释中的两个名词——函数表达式函数声明。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。

    image.png
    console.log(f1)  //function f1(){}
    function f1(){} 
    

    上例是函数声明,解析文件的过程中是会把声明先读取,也就是浏览器知道了这个调用的函数是存在的

    就算是在js文件中函数调用在前,函数声明在后,这也是没有问题的

    console.log(f2); //undefined
    var f2 = function(){}
    

    上例是函数表达式,实际上也是一个var一个函数的过程

    解析函数的时,浏览器会把var 的东西先提到最前面

    即var f1 = undefined,没有定义的,你调用它的函数有什么作用呢?

    所以执行肯定出现了错误

    在script文件中,在文件执行或者函数执行的最开始,浏览器会最先读取函数/变量的声明,将一些赋值的变量也拿出来,也就是我们说的声明提升(只有var 声明的变量才会提升,let,const的不会)

    我们要知道,在准备工作中完成了哪些工作:

    • 变量、函数表达式——变量声明,默认赋值为undefined;
    • this——赋值;
    • 函数声明——赋值;

    这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

    为证明我们的观点,在控制台写上以下代码,你会看到

    于是我们就可以理解下面两种不同的情况
    情况1

    fn('zhangsan');
    function fn(name){
       alert(name);
    }  //运行后没有错误,并且网页弹出警告框
    

    情况2

    f1('zty');
    var f1 = function(name ){
      alert(name)
    }
    
    函数.png

    注意:函数声明就是以function开头的,函数赋值则不是
    函数声明中的函数可以是匿名的,函数赋值不可以。

    让我们来看看一道比较经典的题:

        function yideng() { 
            console.log(1);
        }
        (function () { 
            if (false) {
                function yideng() {
                    console.log(2);
                }
            }
            yideng();
        })();
        //yideng is not a function
    

    在早期的浏览器中,弹出来的结果应该是2,(因为变量提升,整个 function yideng() { }会被提升到整个函数作用域的顶端)但是如果弹出来是2的话,我们写的if语句就完全没有意义了。后来浏览器为了修正这个错误,像这种情况下面的函数提升就只是 var yideng提升到函数作用域的顶端,本题中false所以没进入函数体,所以yideng()就会报错yideng is not a function。而不是像第一题那样,整个函数体都提到前面。
    据说除了if,好像while,switch,for也一样。

    关于this

    http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
    上面这个是阮一峰老师的关于一些this的内部机制的理解

    在箭头函数出来之前,我们只知道this要在函数执行时才能知道指的是什么

    主要场景

    1. 作为构造函数执行
        function Foo(name){        
            this.name = name;
            alert(name);
            console.log(this.name);
        }
        var f1 = Foo('zhangsan');  //控制台上面打印出zhangsan
        var f2 = Foo('hello'); //打印出hello
    

    在这里,this的存在就显得比较有意义了,很多个对象都调用这个相同的方法,this到时指向很多对象,可以灵活使用,也就不用写那么多的一样的函数了

    1. 作为对象属性执行
        var obj = {
            name:'a',
            fn: function(){
                console.log(this.name);
          }
        }
         obj.fn();   //a
    

    这时候的this指向执行这个属性的对象 ,所以也就是a
    想想,如果不是作为obj的属性来调用时,会是怎么样的一种情况?

        var obj = {
            name:'a',
            fn: function(){
                console.log(this.name);  
                console.log('kkk');
          }
        }
        var fn1 =  obj.fn;  
        fn1()//undefined
    

    正如上面的代码:如果fn函数被赋值到了另一个变量中,并没有作为obj的一个属性被调用,那么this的值就是window,this.x为undefined。

    1. 作为普通函数执行
        function fn(){
            console.log(this);
        }
        fn(); // this指向全局对象
    
    1. call bind apply
        function fn1(name,age){
              console.log(name);
              console.log(this);
            }
        fn1.call({x:100},'zhangsan',20);
        //执行结果:zhangsan  {x:100)}
        //也就说call能让this指向它所指定的东西,在例子中 就是{x:100}
        //常用的就是call.
        apply用法与call相同,但是后面是以数组形式传递的
        fn1.call({x:200},['zhangsan',20]);
    
        var f2 = function(name){
              console.log(name);
              console.log(this); 
        }.bind({y:100},'lisi')
    

    其实关于js中的this一直是前端一个经久不衰的话题,很多笔试题都在this上面做文章。

    this指向的四种情况我们就大概讲到这里,来做几道课后题吧!

    var obj = {
        x: 10,
        fn: function(){
            function f(){
                console.log(this);//Window
            }
            f()
        }
    };
    obj.fn()
    

    什么?答案不是obj吗,不是说作为对象属性来执行的时候是指向对象的吗?


    别急,且听我慢慢道来!

    函数f虽然是在obj.fn内部定义的,但是它仍然是一个普通的函数,this仍然指向window。作为对象属性来执行的时候是指向对象的这句话在这个函数里指的是fn而不是f,fn是在obj里面定义的,但f是在obj.fn里面定义的。不知道这个解释你看懂了吗?

    var x = 3;
    var y = 4;
    var obj = {
        x: 1,
        y: 6,
        getX: function() {
            var x =5;
            return function() {
                return this.x;
            }();
        },
        getY: function() {
            var y =7;
            return this.y;
        }
    }
    console.log(obj.getX()) 
    console.log(obj.getY())
    

    先给两分钟的时间思考一下,这道题的结果应该是:
    3 6

    image.png

    让我们来探究一下吧!
    obj.getX跟我们上面说的题其实差不多,this所在的function其实只是定义在obj.getX中的一个普通函数,this自然是指向Window的,从作用域的角度来看,他会先在第一层寻找自己的this.name,找到了,匿名函数的执行环境具有全局性,而且这个时候是指向window的,因为这是一个普通的函数。所以应该是3。
    obj.getY就很循规蹈矩了,getY就是obj里面的一个属性,作为obj的属性执行时指向obj,obj的作用域里面的x就等于6。

    我们继续。

    var length = 10;
    function fn(){
    console.log(this.length);
    }
    var obj = {
        length:5,
        method:function(fn){
        fn();
        }
    }
    obj.method(fn);
    

    答案是10,一样的道理。function fn其实是指向全局的啦!

    this指向也是ES6箭头函数的一个重要的特性,下次再单独用一篇文章来写。

    关于作用域

    说到JS其中一座大山,就是我们大名鼎鼎的闭包了。
    不过在说闭包之前,我们要先说说作用域!

    首先应该明确,Js无块级作用域

        #include <stdio.h> 
        void main() { 
          int i=2; 
          i--; 
          if(i) { 
            int j=3; 
         } 
          printf("%d/n",j); 
        }
    

    以C语言为例,这个会弹出错误,因为j是在if的块中定义的,如果跳出了这个块,是访问不到的。
    但是在Js中,不同块中的还是可以访问的到的。

    if(true){
      var i = 7;
    }console.log(i);//7
    

    父级作用域

    var a = 100;
        function fn1(){
            var b = 200;
            console.log(a);
            console.log(b);
        }
    

    fn1的父级作用域是window

    函数里面定义的数,在外面是改变不了的,外面是污染不了里面的数的。看下面的题:

    var a = 100;
    function f1(){
      var b = 200;
      console.log(a);
      console.log(b);
    }
    var a = 10000;
    var b = 20000;
    f1();//10000   200;
    

    javascript除了全局作用域之外,只有函数可以创建的作用域。

    所以,我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。

    之前就踩了一个坑!

    let person = {
        a:2,
        say:()=>{
            console.log(this.a)
        }
    }
    

    因为我把方法写在了对象里,而对象的括号是不能封闭作用域的。所以此时的this还是指向全局对象,所以不是指向person。

    让我们继续说一下作用域:

    image.png

    如我们上图:全局,fn,bar都分别有自己的作用域,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

    作用域的作用就是能隔离变量,不同作用域下同名变量不会有冲突。bar需要a就拿bar下面的a,fn需要a就拿fn下面的a,全局需要就拿全局的a。

    ES6的let和我们所谓的块结合就能达到我们所期待的块级作用域的效果了。

    作用域和上下文环境

    闭包

    http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

    闭包使用场景:
    1. 函数作用一个返回值
        function f1(){
            var a = 100;
            return function(){
                console.log(a);
            }
        }
    
        var f2 = f1(); //f2(){console.log(a)},执行时候去父作用域中寻找(定义时候的)
        var a = 200;//这里的a 与上面的a 是两码事
        f2(); //100,
    
    1. 函数作为一个参数来传递
            function f1(){
            var a = 100;
            return function(){
                console.log(a);
            }
        }
    
        var f2 = f1();
                function f3(fn){
                    var a = 200;
                    fn();
          }
          f3(f2); //100
    
    闭包的特殊之处

    先看一下这段代码:

        function compare(value1,value2){
            if(value1<value2){
                return -1;
            }else if(value1>value2){
                return 1;
                }else{
                    return 0;
                      }
         }
        var result = compare(5,10)
    

    上面先定义了compare()函数,然后在全局作用域中调用了他。

    当第一次调用compare()的时候,会创建一个包含this,arguments.value1,value2的活动对象,(位于compare()函数作用域链中的第一位),全局执行环境的变量对象(this,result,compare)在compare()函数作用域的第二位。

    后台的环境都有一个表示变量的对象——变量对象,全局环境的变量对象始终存在,像compare()函数的变量对象,是局部变量的,是只在函数执行过程中存在。创建compare这个函数的时候,会先创建一个预先包含全局变量对象的作用域链,保存在内部的scope属性中,当你执行compare这个函数的时候,会先为函数创建一个执行环境,然后将scope属性中的对象复制进行你作用域链的构建,此后又有一些函数内部的一些变量对象被推进到作用域链的前端,在compare()这个函数中,其作用域就包括全局变量和局部变量。作用域链本质上是一个指向变量对象的指针列表。

    一般来讲,函数执行完毕,局部变量(活动对象)就会被销毁。内存中仅保存全局作用域链,但是闭包的情况有所不同。

    出现闭包的情况时,在函数内部定义的函数会将外部函数的活动对象添加到它的作用域中,在下面的代码中,匿名函数从f1()中被返回的时候,他的作用域链也被初始化为f1函数的活动对象的全局变量对象,匿名函数可以访问所有在f1中定义的所有变量,更重要的是,在f1函数执行完毕的时候,他的活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象,直到匿名函数被销毁之后,f1的活动对象才会被销毁。

      function f1(){
        var a = 100;
        return function(){
        console.log(a);
        }
      }
      var f2 = f1()  //利用上面这个函数来创建一个新的函数
      var result = f2()//调用函数
      f2 = null //解除对匿名函数的引用。
    

    相关面试题目

    说一下对于变量提升的理解

    Js中函数和变量的声明总是会被解析器提到方法体的最顶部。(注意,函数表达式不会哦)看上文的关于作用域这一小节
    最简单的例子:

    console.log(f);
    var  f = 100;  //undefined,被提到最前面去的只是var f(l类型是undefined)
    
    var e = 100;
    console.log(e);  //100
        
    fn('zhangsan');
    function fn(name){
      this.name = name;
      alert(this.name);
     }//运行的时候弹zhangsan,因为函数已经被声明了,function fn以及被提到最前面去了
    
    f5('xiaosan');
    var f5=function(name){
      this.name = name;
      alert(this.name);
     } //出现错误,因为这是函数的赋值而不是声明
    
    闭包块级作用域

    一般会给出这样的代码,让你看看可能的运行结果

    请写出如下点击的输出值,并用三种办法正确输出li里面的数字。

    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
    </ul>
    <script type="text/javascript">
        var list_li = document.getElementsByTagName("li"); 
        for (var i = 0; i < list_li.length; i++) {
            list_li[i].onclick = function() { console.log(i);
            }
        }
    </script>
    

    真的是很经典的一道题啊!!!!大家可以闭着眼睛说不管点击哪个li都只会输出6。(跟Js的单线程和异步队列有关)

    解决方法有三个:

    • ES6的let
        for (let i = 0; i < list_li.length; i++) {
            list_li[i].onclick = function() { 
                console.log(i+1);
            }
        }
    

    关于let 可以看看暂时性死区这个知识点。

    • 闭包,立即执行函数
      for(var i = 0; i < list_li.length; i++){
        (function(i){
            list_li[i].onclick = function(){
                console.log(i+1)
            }
        })(i)
      }
    

    闭包保存这个变量。

    • 最后一种方法是佳哥说出来的时候恍然大悟,这才是最in的解决方案啊我靠。这道题考的是this啊,不是闭包也不是作用域。。。
        for (var  i = 0; i < list_li.length; i++) {
            list_li[i].onclick = function() { 
                console.log(this.innerHTML);
            }
        }
    
    补充知识

    立即执行函数会为每次循环创建一个单独的作用域

    立即执行的函数表达式,只要定义完成,不需要调用,马上执行
    (function(){ })(i)

    var a = 2;
    (function foo(){
    var a = 3;
    console.log(a);//3
    })();
    console.log(a);//2
    

    函数表达式后面加上一个括号会立即执行。
    有一个非常普遍的进阶用法,就是把他们当作函数调用并且传递函数进去

    var a = 2;
    (function IIFE(global){
        var a = 3;
        console.log(a);//3
      })(window);
      console.log(a);//2
    
    如何理解作用域
    1. 自由变量

    2. 作用域链,即自由变量的查找

    3. 闭包的两个场景

       var a = 100;
           function f1(){
           console.log(a);//此时在函数作用域中找不到a,我们把a 称为自由变量
       }
      

    在f1作用域中找不到a ,我们就沿着作用域链去上一级寻找,a的父级作用域就是window(看f1是在哪里定义的)然后我们找到了var a = 100;于是开开心心的打印出 100

    1. 场景1:函数作为参数被传递
      场景2:函数作为一个返回值。
      一般大家是这么形容闭包的:当一个函数的返回值是另外一个函数,而返回的那个函数如果调用了其父函数内部的其他值,如果返回的这个函数在外部被执行,就产生了闭包。
      闭包是指有权访问另外一个函数作用域的变量的函数。
      创建闭包的常见方式,就是在一个函数内部创建另外一个函数
      例如下面的例子,外部执行了这个函数,是可以访问的到a 这个变量的,之所以能够访问得到这个变量,主要还是因为内部函数的作用域中包含着外面这个函数f1的作用域。

       function f1(){
           var a = 100;
           return function(){
               console.log(a);
           }
       }
      
       var f2 = f1(); //f2(){console.log(a)},执行时候去父作用域中寻找(定义时候的)
       var a = 200;//这里的a 与上面的a 是两码事
       f2(); //100
      

    本质上无论何时何地,如果将函数当作第一级的值类型并且到处传递,你就会看到闭包在这些函数中的应用。在定时器,事件监听器,Ajax请求,跨窗口通信,Web Worker或者其他任务中,只要使用了回调函数,实际上就是在使用闭包。

    下面是一个由于没有理解闭包和作用域而可能会遇到的坑

        for(var i = 1;i <=5;i++){
            setTimeout(function timer(){
                console.log(i);
            },i*1000)
        }//弹出来了5个6
    

    与我们的预期不一样,我们预期是依次弹出1,2,3,4,5
    根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭
    代中分别定义的,像上面的代码中,他们都被封闭在一个共享的全局作用
    域中,每次循环,每个函数都保存着全局作用域中的活动对象,其中包括i
    值,也就是说每个函数的作用域链中都保存着全局作用域中的i,当主线程
    中的i++这些操作结束之后,i已经变成6了,闭包只能取得包含函数中任何
    变量的最后一个值也就是6,在时间执行到了timer函数的时候,因为js解析
    的时候遇到setTimeout会把回调抽出来,放在一个单独的队列里面,等到
    当前处理机空闲了,再去执行那个队列里的东西。处理机先将for循环全都
    处理完毕,再去执行SetTimeout这个函数。此时全局变量的这个i就是6,
    因此无法达到预期。

    解决方法

    for(var i = 1;i<=5;i++)
        (function(i){
            setTimeout(function timer(){
                console.log(i);  //i是自由变量,去父级作用域里面找,我们找到了立即函数传进来的i(每次for循环后都执行一次立即函数)
    即函数为每次循环都开拓一个单独的作用域(块级作用域),在ES6中只要用{  }就是一个块级作用域了,不用怕会被覆盖
            },i*1000)
        })(i);
    

    其实将i 前面的var 改成let 也是可以得到我们想要的效果,因为let声明的也是一个块级作用域。

    上面的区别在于,我们加了一个自执行函数,只要定义,不需要调用,而且可以立马执行,还会产生一个专门的块级作用域,(function(i))(j)就是把j的值传给i,产生了这么一个块级作用域。
    闭包在实际开发中的作用
    模块化中经常需要用到闭包
        function f1(){
            var something = "something";
            return function(){
                console.log(something);
            }
        }
    
        var foo = f1();
        foo(); //外面的根本触碰不到函数里面的变量
    
    实际运用2

    用于封装变量,收敛权限。

        function firstLoad(){
            var _list = [];//变量前面带有_表明变量是私有变量
            return function(id){
                if(_list.indexOf(id) >= 0){
                    return false;//这个Id对应的位置上已经有东西
                }else{
                    _list.push(id);//这个Id对应的位置上压东西进去
                    return true;
                }
            }
        }
    
        var load = firstLoad();
        load(10); //true ,第一次加载.
        load(10); //false,因为上面已经加载过了
    

    主要就是只有通过firstload返回的函数才能去操作__list这个数组,如果你在外面直接去操作__list是不可能的。因为__list是没有被直接暴露出来的。

    理解了这两段代码的结果,也许你就理解了什么是闭包。

    var name = "The Window";
    var object = {
    name:"my object",
    getNameFunc:function(){
    return function(){
    return this.name;
    };
    }
    };
    console.log(object.getNameFunc()());//var name = "The Window";
    
    var name = "The Window";
    var object = {
    name:"my object",
    getNameFunc:function(){
    var that = this;
    return function(){
    return that.name;
    };
    }
    };
    object.getNameFunc()();
    // "my object"
    

    谈谈我自己的理解,因为我们首先要看两个函数其实都产生了闭包,但是在return function()中return this.name,我们要注意到这其实是一个匿名函数,从作用域的角度来看,他会先在第一层寻找自己的this.name,找到了,匿名函数的执行环境具有全局性,而且这个时候是指向window的,因为这是一个普通的函数。

    第二段代码中,将getNameFunc作用域里面的this保存在闭包可以访问到的变量里,就可以让闭包访问这个对象。(argument也是同样一个道理)

    (也就是上面讲的作为函数对象执行)

    var obj={name:'hhhh',
    func:function(){
    return this.name;}}
    obj.func()//"hhhh"
    

    //至于为什么是getNameFunc()();

    var object = {
    name:"my object",
    getNameFunc:function(){
    return function(){
    return this.name;
    };
    }
    };
    console.log(object.getNameFunc());
    // ƒ (){ return this.name; }
    

    一个括号其实就是得到的return的值是那个function
    再加一个括号就是再去执行那个function

    闭包为什么会造成内存泄漏

    先谈谈计数垃圾收集,如果0个引用指向他,那么该对象就可以被垃圾收集了。但是计数垃圾收集有一个严重的问题,就是循环引用的对象是不能回收的,IE9之前就是用javascript引擎就是使用标记清除策略来实现。
    循环引用(两个对象的相互引用),在函数调用之后他们是无用的可以释放,但是计数算法认为,由于两个对象的每一个至少都是被引用一次的,两者都不能被垃圾回收。例如下面的:

        function problem(){
            var ObjectA = new Object();
            var ObjectB = new Object();
    
            ObjectA.someOtherObject = ObjectB;
            ObjectB.someOtherObject = ObjectA;
            }
    

    在IE9之前,BOM和DOM对象都是使用C++以COM对象的形式来实现的,COM对象的垃圾收集机制采用的就是计数策略,所以IE中只要涉及到BOM和DOM对象,就会存在循环引用的问题。
    像下面这样子:

          var element = document.getElementById("some_element");
          var myObject = new Object();
          myObject.element = element;
          element.someObject = myObject;
    

    闭包实际上很容易造成JavaScript对象和DOM对象的隐蔽循环引用。也就是说,只要闭包的作用域链中保存着一个HTML元素,那么就意味着这个元素将无法被销毁。

          function assignHandler(){
            var element = document.getElementById("someElement");
            element.onclick = function(){
                alert (element.id);
           }; 
        }
    

    注意,闭包的概念是一个函数的返回值是另外一个函数,返回的那个函数如果调用了其父函数内部的其他值,这是一个element元素事件处理的一个闭包。这个闭包创建了一个循环引用。

    1. JavaScript对象element引用了一个DOM对象(其id为“someElement”); JS(element) ----> DOM(someElemet)
    2. 该DOM对象的onclick属性引用了匿名函数闭包,而闭包可以引用外部函数assignHandler 的整个活动对象,包括element ; DOM(someElement.onclick) ---->JS(element)
      匿名函数一直保存着对assginHandler()活动对象的引用,它所占的内存永远不会被回收。

    所以可以稍稍改进:

            function assignHandler(){
            var element = document.getElementById("someElement");
            var id = element.id;
            element.onclick = function(){
                alert (id);
           }; 
           element = null;
        }
    

    可以首先通过引用一个副本变量消除循环引用,但是这个闭包包含外部函数的全部活动对象,所以就算不直接引用,匿名函数一直都包含着对element的引用。所以最后需要手动设置element变量为null.

    闭包的很大作用是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

    只要变量被任何一个闭包使用了,就会被添到词法环境中,被该作用域下所有闭包共享。

    关于函数提升
        alert(a)    1
        a();   2
        var a=3;
        function a(){
            alert(10)
        } 
        alert(a)    3
        a=6;
        a();   4
    

    请写出弹出值,并解释为什么?
    由于函数提升,整个式子会变成下面这样

      var a;
      function a(){
         alert(10)
      } 
      alert(a)    1
      a();   2
      a=3;
      alert(a);   3    
      a=6;
      a();   4
    

    很明显,会得到下面的结果
    1. f a(){alert(10)} //变量提升,并且函数优先级高于变量,如果相同命名,变量会被覆盖。
    小知识:

        console.log(c)//ƒ c(){console.log('hahha') 
        function c(){
            console.log('hahha')
        }
        var c = 1;
    
        console.log(c) //ƒ c(){console.log('hahha') 
        var c = 1;
        function c(){
            console.log('hahha')
        }
    
    1. 10 //执行了a(){alert(10)}
    2. 3 //给a赋值了3
    3. a is not a function 此时已经给a赋值了6,a已经不是函数了,所以会弹出错误
    关于函数变量提升的一些特殊情况
        function yideng() { 
            console.log(1);
        }
        (function () { 
            if (false) {
                function yideng() {
                    console.log(2);
                }
            }
            yideng();
        })();
        //yideng is not a function
    在早期的浏览器中,弹出来的结果应该是2,(因为变量提升,整个         function yideng() { }会被提升到整个函数作用域的顶端)但是如果弹出来是2的话,我们写的if语句就完全没有意义了。后来浏览器为了修正这个错误,像这种情况下面的函数提升就只是 `var yideng`提升到函数作用域的顶端,本题中false所以没进入函数体,所以yideng()就会报错yideng is not a function。而不是像第一题那样,整个函数体都提到前面。
    

    据说除了if,好像while,switch,for也一样。
    补充小知识:

        if(false){
            var a = 1;
        }
        console.log(a)//undefined
    
    关于this

    写出以下输出值,解释为什么?

    this.a = 20; 
    var test = {
        a: 40,
        init:()=> {
            console.log(this.a);  
            function go() {
                console.log(this.a);
            }
            go.prototype.a = 50; 
            return go;
            }
        };
        new(test.init())();   //20   50 
    

    箭头函数的this绑定父级的词法作用域,所以他的this.a固定了是20。
    go本身没有a 这个属性,所以new出来的对象也没有这个属性,会到原型链上面去找。所以是50。

      this.a = 20; 
      var test = {
        a: 40,
        init:()=> {
            console.log(this.a); 
            function go() {
               this.a = 60;
                console.log(this.a);
            }
            go.prototype.a = 50; 
            return go;
            }
        };
        var p = test.init();  //20 
        p();  //  60 
        new(test.init())();   //  60,60
    

    刚开始我以为输出的结果会是20 60 20 60
    var p = test.init(); 此时test.init()会执行一遍。此时的箭头函数中的this绑定了最外层的this(test父级作用域的this),也就是this.a = 20
    p() 此时相当于执行go(),注意:这个时候是var p = test.init(),所以执行p()时的this是指向windows,也就是我们现在看到的最外层的this.a = 20的那个this,此时会执行this.a = 60,将windows的this.a 更改成60,执行之后打印出来的this.a=60.
    new(test.init())():test.init()会取到已经绑定了的父级词法作用域的this,不过此时的this.a已经被改为60,所以打印出来60。继续执行,打印出60。

    思考一下,如果是下面这种情况的话,会打印出来什么呢?

    image.png

    执行的时候20
    因为这里是var 不是this.a

    另外一个小知识:

        function test(){
            console.log(this)
        }
        var obj = {
            f:test
        }
        (obj.f)()  //Cannot read property 'f' of undefined
    

    ?????什么鬼??
    原来是因为没有加;在浏览器看来
    var obj = {f:test}(obj.f)() 是这样连在一起的。那肯定会报错啊!

    加上分号之后,我们再来看一下这段代码运行的结果是怎么样的。

        function test(){
            console.log(this)
        }
        var obj = {
            f:test
        };
        (obj.f)()//{f: ƒ}
    

    很明显,指向了obj这个对象,因为是obj调用了test()。
    那如果我们再这样改一下:

        function test(){
            console.log(this)
        }
        var obj = {
            f:test
        };
        (false || obj.f)()
    

    猜猜结果是什么?是window
    ?????这又是什么鬼???
    短路语句的结果不也是obj.f,运算的结果应该也是obj才对啊。
    但是()里面是计算,所以里面应该是表达式,也就是说里面相当于var xx = obj.f,然后xx(),此时this肯定是指向全局变量window

    关于闭包块级作用域

    请写出如下点击的输出值,并用三种办法正确输出li里面的数字。

    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
    </ul>
    <script type="text/javascript">
        var list_li = document.getElementsByTagName("li"); 
        for (var i = 0; i < list_li.length; i++) {
            list_li[i].onclick = function() { console.log(i);
            }
        }
    </script>
    

    真的是很经典的一道题啊!!!!大家可以闭着眼睛说不管点击哪个li都只会输出6。(跟Js的单线程和异步队列有关)
    解决方法有三个:

    • ES6的let

        for (let i = 0; i < list_li.length; i++) {
            list_li[i].onclick = function() { 
                console.log(i+1);
            }
        }
      

    关于let 可以看看暂时性死区这个知识点。

    • 闭包,立即执行函数

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

    闭包保存这个变量。怎么去理解闭包保存这个变量呢,也就是说这里每次执行立即执行函数的时候,会传递一个i(实参)进去函数级的作用域,在这个函数级作用域里面onclick的回调函数使用了这个i,也就是我们常说的闭包了,还要注意的一点是i在这里的按值传递的,所以每个i都是独立的各不影响的。

    • 最后一种方法是佳哥说出来的时候恍然大悟,这才是最in的解决方案啊我靠。这道题考的是this啊,不是闭包也不是作用域。。。

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

    内存泄漏

    http://www.ruanyifeng.com/blog/2017/04/memory-leak.html

    相关文章

      网友评论

        本文标题:作用域与闭包

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