美文网首页
第一部分 第5章 作用域和闭包

第一部分 第5章 作用域和闭包

作者: 酥枫 | 来源:发表于2018-10-14 13:02 被阅读0次

    闭包概念

    有几种解释,大致意思是一样的

    • 当函数可以记住并且访问所在的词法作用域时,就产生了闭包
    • 闭包是函数和声明该函数的词法环境的组合
    • 闭包是指有权访问另一个函数作用域中的变量的函数

    创建闭包的常见方式,就是在一个函数内部创建另一个函数,下面就是一个简单的闭包:

    function foo(){
        var a=2;
        function bar(){
            console.log(a);
        }
        return bar;
    }
    var baz=foo();
    baz();//2
    

    foo()执行之后,通常会期待foo的整个内部作用域被销毁,当作垃圾回收,但是由于内部函数bar中引用了foo中的变量a,并且作为返回值返回,使得外部作用域中的一个变量baz获得了该返回值的引用,导致foo的作用域没有被销毁,bar依旧可以访问foo的词法作用域(这里要注意,是访问词法作用域,但是却访问不了foo的this指针,下文会细说),直到baz()执行之后才会被销毁。

    下面再看一个例子:

    var name="window name"
    function wait(){
        var anotherValue="hahaha";
        setTimeout(function timer(){
            console.log(anotherValue);
            console.log(this.name);
        },2000);
    }
    wait();
    //两秒之后输出:
    //hahaha
    //window name
    

    这也是一个闭包,在wait()执行完毕之后没有被垃圾回收机制回收,因为内部的timer保留着对wait词法作用域的引用,所以在wait执行之后,内部作用域并不会消失,直到2000毫秒之后setTimeout将timer函数添加到时间循环队列中且timer执行完毕之后才会销毁作用域。

    有个问题,timer中的this指向哪里呢,this引用的是什么?通过上面的输出可以看到此时this引用的是全局对象window,下面我们通过两个例子来详细看看闭包中的this:

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

    在上面两个例子中,我们可以看到,虽然闭包可以保留对它的包含函数作用域的引用,但是this的指向还是遵循那几条绑定规则的(this的绑定规则在第二部分详细讲)。
    在第一个例子中,object,getNameFunc()()相当于:

    (function(){
        return this.name;
    })();
    

    此时this应用的是默认绑定,绑定到了全局对象,所以输出The Window。
    在第二个例子中,object,getNameFunc()应用了this的隐式绑定,此时this指向object,然后用that这个词法作用域的变量来记录此时的this,最后在object,getNameFunc()()执行时,可以访问内部作用域中的that,所以输出为My Object。然后将object.getNameFunc的引用赋值给foo,再执行foo()时this绑定到了全局对象(这里是this的隐式丢失),所以最后foo()()执行结果为The Window。
    这里最大的区别是,that为手动声明的一个词法作用域的变量,闭包可以访问所在词法作用域,但是this的绑定却是动态的。

    循环和闭包

    考虑下面例子:

    for(var i=0;i<=2;i++){
        setTimeout(function timer(){
            console.log(i);
        },1000);
    }
    

    这里期望的是在1000毫秒之后输出0 1 2,但是实际情况确实在1000毫秒之后输出3 3 3,这是为什么呢?
    因为延迟函数里的回调函数会在循环结束之后才执行,就算定时器设置的是setTimeout(...,0),所有的回调函数也都是在循环结束之后才会执行,而因为用JavaScript中不存在块作用域,看似在每个迭代中分别定义了五个函数,实际上上面的代码相当于:

    {
        var i=0;
        setTimeout(function timer(){
            console.log(i);
        },1000);
        i++;
        setTimeout(function timer(){
            console.log(i);
        },1000);
        i++;
        setTimeout(function timer(){
            console.log(i);
        },1000);
        i++;
    }
    

    这个块结束之后没有被销毁,因为这几个timer函数引用了它里面的变量,形成了闭包,但因为没有块作用域,这几个timer函数引用的是同一个作用域(为了方便理解,我把这个作用域用{}括起来了),也就是说引用的i只有一个,引用的是同一个i,所以输出了三个3。

    为了达到预期的效果(输出0 1 2),应该怎么做呢,首先想到的就是,既然是因为没有块作用域造成的,那么就通过创建块作用域来解决,这里有两种办法,一个是用IIFE立即执行函数,另一个是用let:

    • IIFE
      下面这三种形式本质上都是一样的,都是为了创建块作用域,第三个稍微复杂点。
      for(var i=0;i<3;i++){
          (function(){
              var j=i;
              setTimeout(function timer(){
                  console.log(j);
              },1000);
          })();   
      }
      
      for(var i=0;i<3;i++){
          (function(j){
              setTimeout(function timer(){
                  console.log(j);
              },1000);
          })(i);   
      }
      
      for(var i=0;i<3;i++){
          setTimeout((function(j){
              return function(){
                  console.log(j);
              }
          })(i),1000);
      }
      
    • let
      下面两种形式也是一样的,不过对于第二种直接在for循环头部进行let声明还会有一个特殊行为,这个行为之处变量在循环过程中不知被声明一次,每次迭代都会声明,且随后的每个迭代都会使用上一个迭代结束是的值来初始化这个变量(即i++)。
      for(var i=0;i<3;i++){
          let j=i;
          setTimeout(function timer(){
              console.log(j);
          },1000);
      }
      
      for(let i=0;i<3;i++){
          setTimeout(function timer(){
              console.log(i);
          },1000);
      }
      
      有可能看来上面两张方法你还不是特别理解,其实上面的IIFE也好,let声明也好,都大致相当于:
      {
          var i=0;
          {
              let j=i;
              setTimeout(function timer(){
                  console.log(j);
              },1000);
          }
          i++;
      
          {
              let j=i;
              setTimeout(function timer(){
                  console.log(j);
              },1000);
          }
          i++;
          
          {
              let j=i;
              setTimeout(function timer(){
                  console.log(j);
              },1000);
          }
          i++;
      }
      
      相当于显式地创造了几个块作用域,在每个块结束之后,每个块内部的timer函数形成了闭包,引用了每个块作用域中的j(觉得不好理解的话可以类比前面例子中的wait()函数),直到timer执行完之后才会销毁每个块作用域。

    模块

    模块简介

    运用上面讲到的闭包,我们可以实现模块机制,来看一个简单的例子:

    function Module(){
        var something="cool";
        var another=[1,2,3];
    
        function doSomething(){
            console.log(something);
        }
        function doAnother(){
            console.log(another.join("-"));
        }
    
        return {
            doSomething:doSomething,
            doAnother:doAnother
        };
    }
    var aModule=Module();
    aModule.doSomething();//cool
    aModule.doAnother();//1-2-3
    

    上面代码的这个模式,在JavaScript中被称为模块,通过调用Module()函数来创建一个模块实例,即通过调用这个函数,来创建内部作用域,返回一个保存内部函数引用的对象来形成闭包,这样相当于返回了模块的公共API。

    当只需要一个实例时,可以用单例模式来实现(单例模式其实就是将模块函数转化成了IIFE):

    var sigleton=function(){
        var something="cool";
        var another=[1,2,3];
    
        function doSomething(){
            console.log(something);
        }
        function doAnother(){
            console.log(another.join("-"));
        }
    
        return {
            doSomething:doSomething,
            doAnother:doAnother
        };
    }();
    sigleton.doSomething();//cool
    sigleton.doAnother();//1-2-3
    

    单例模式通常用来为对象创建私有变量和私有函数,如下:

    var sigleton=function(){
        var privateVar=10;//私有变量,外部无法访问
        function privateFunc(){//私有函数
            return false;
        }
    
        return{
            publicVar:"hahaha",//公共变量
            publicFunc:function(){//公共函数
                privateVar++;
                return privateFunc();
            }
        }
    }();
    console.log(sigleton.privateVar);//undefined
    console.log(sigleton.publicVar);//"hahaha"
    console.log(sigleton.publicFunc());//false
    

    当然,模块返回的API也是函数,可以接受参数。甚至可以通过在模块实例内部保留对公共API对象的内部的引用,可以从内部对模块实例进行修改,包括添加或删除方法、属性,或者修改它们的值。

    现代的模块机制

    现在的模块机制通常都是将上面的模块定义封装进一个对使用者友好的API里,形成模块依赖加载器/管理器,下面来简单看下核心实现:

    var MyModules=function(){
        var modules={};
    
        function define(name,deps,impl){
            for(var i=0;i<deps.length;i++){
                deps[i]=modules[deps[i]];
            }
            modules[name]=impl.apply(impl,deps);
        }
    
        function get(name){
            return modules[name];
        }
    
        return {
            define:define,
            get:get
        };
    }();
    

    再来看看如何使用:

    MyModules.define("bar",[],function(){
        function hello(who){
            return "Let me introduce: "+who;
        }
    
        return {
            hello:hello
        };
    });
    
    MyModules.define("foo",["bar"],function(bar){
        var hungry="hippo";
    
        function awesome(){
            return bar.hello(hungry).toUpperCase();
        }
    
        return {
            awesome:awesome
        };
    });
    
    var bar=MyModules.get("bar");
    var foo=MyModules.get("foo");
    
    console.log(bar.hello("hippo"));//Let me introduce: hippo
    console.log(foo.awesome());//LET ME INTRODUCE: HIPPO
    

    简单讲解一下,MyModules是一个模块管理器,有两个公共API(define和get),一个用来定义模块,并将模块存储到MyModules内部,另一个用来根据模块名获取(从MyModules内部取出)模块。get没什么好说的,重点来看看define这个函数。
    define(name,deps,impl)函数接受三个参数,通过第三个参数impl(一个函数,相当于前面讲到的Module()函数)来生成(或者说返回)名字为第一个参数name的模块,而第二个参数则是impl在生成模块时会用到的(依赖到的)其他(存储在MyModules这个管理器内部的)模块列表(是一个数组,里面列出了所有会依赖到的模块)。在define函数中,首先通过for循环来讲第二个参数中列出的模块从MyModules中取出放入deps数组中,最后,通过impl.apply(impl,deps)来最终生成名字为name的模块,并放入MyModules中。

    未来的模块机制

    未来的模块机制主要是指在ES6中引入的对模块的语法支持。ES6的模块没有“行内”格式,必须被定义在独立的文件中(一个文件是一个模块),浏览器或引擎有一个默认的模块加载器可以在导入模块是同步加载模块文件。如下:

    bar.js

    function hello(who){
        return "Let me introduce: "+who;
    }
    export hello;
    

    foo.js

    //仅从“bar”模块导入hello()
    import hello from "bar";
    var hungry="hippo";
    function awesome(){
        return hello(hungry).toUpperCase();
    }
    export awesome;
    

    baz.js

    //导入完整的“foo”和“bar”模块
    module foo from "foo";
    module bar from "bar";
    console.log(bar.hello("hippo"));
    console.log(foo.awesome());
    

    import可以将一个模块中的一个或多个API导入到当前的作用域中,并分别绑定在一个变量上(在上面的例子中是hello)。module会将整个模块的API导入并绑定到一个变量上(在上面的例子中是foo和bar)。export会将当前模块的一个标识符(变量或者函数)导出为公共API。
    模块文件中的内容会被当作好像包含在作用域中一样来处理,就和前面讲的函数闭包模块一样。

    相关文章

      网友评论

          本文标题:第一部分 第5章 作用域和闭包

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