当前位置: 移动技术网 > IT编程>开发语言>JavaScript > js闭包的使用详解

js闭包的使用详解

2018年09月17日  | 移动技术网IT编程  | 我要评论

独立作用域

在es6出现之前,js中并没有块级作用域的存在,这意味者单纯一个大括号并不能隔离出一块作用域

   {
      var a = 1;
   }

这样的大括号没有隔离出一块作用域,那么变量a声明在括号内或者括号外都是一样的,那么js中什么时候能隔离出一个局部作用域呢,答案是函数

   var b = 1;
   function fn(){
      var a = 1;
      console.log(b);   //1
   }
   console.log(a)   // a is undefined

这时候函数单独隔离出了一个作用域。而函数外面的作用域在函数作用域的外层,因而函数内部能够访问到外部变量b,但函数外作用域无法访问函数内变量a。
那么在同时声明了变量a的时候会怎么样呢

   var a = 1;
   function fn(){
      var a = 2;
      console.log(a)   
   }
   fn();    // 2
   console.log(a)   // 1

当函数内部作用域再次声明变量a的时候,这时候变量a的新声明被压入函数调用栈中,这时js引擎读取a的值时候,会读取到新的声明,所以a的值是2。而执行完函数,局部作用域的a就被弹出(变量a的生命周期结束)。上下文切换到外部作用域之后,a的值就是原来外部作用域中的a,因此输出1。
同样,把fn函数替换成一个立即执行函数(学名缩写为iife)效果相同

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

闭包

之前说到,函数可以访问外部作用域中的变量,但外部作用域不能访问函数内部变量。

   function fn1(){
      var a = 2
      function fn2(){
         console.log(a);   
      }
      return fn2;
   }
   var fn3 = fn1();
   fn3();      // 2 这就是闭包

上面代码的fn2可以轻松访问到变量a,这个毫无疑问。当fn2的引用被赋值给fn3,那么fn3现在和fn2一样,能访问到变量a,这个也毫无疑问。然而fn3的声明却在外部作用域,这和我们上文说的外部作用域不能访问到函数内部变量相悖,这,就是闭包。
由于内部函数fn2和fn3的特殊关系,原本fn1的内部作用域原本会被销毁并被js引擎的垃圾回收器回收内存,现在fn1却能一直存活。

顽强的闭包

内部函数fn2的引用无论被传递到哪个作用域中,它都会持有对原始作用域的引用,也就是说,一直能读取到变量a

   var fn4;
   function fn1(){
      var a = 2
      function fn2(){
         console.log(a);   
      }
      fn4 = fn2;
   }
   function fn3(){
      fn4();     // 还是强行输出了2
   }
   fn3();

闭包无处不在

在定时器,事件监听器,ajax请求或者其他异步任务中,只要使用了回调函数,实际上就是在使用闭包(回调函数被扔在事件队列中,还保存着对msg等变量的作用域引用)

   function fn(msg){
      settimeout(function(){
          console.log(msg);
      }, 1000);
   }
   fn('hello');

fn执行1000毫秒之后,它的内部作用域并不会消失,依然拥有对fn作用域的闭包。

   var btn = document.getelementbyid('button');
   var action = 'click';
   function fn(btn, action){
       btn.onclick = function(){
           console.log(action);
       }
   }
   fn(btn, action);   // 每次点击都能得到action

有一个比较常见的场景是,给循环的元素绑定事件监听函数

   var nodes = document.getelementsbytagname('p');
   for(var i = 0, len = nodes.length; i < len; i++){
       //这里通过一个iife封闭一个关于i的内部作用域
       (function(i){
           nodes[i].onclick = function(){
               //click回调函数中通过闭包拿到i变量
               alert(i);
           } 
       })(i)
   }

内存泄漏

   function handler(){
       var element = document.getelementbyid('someelement');
       var id = element.id;
       element.onclick = function(){
          alert(id);
       }
       //只要onclick的回调匿名函数存在,element所占的内存就永远不会被回收,而我们这里只需要变量id,所以我们需要把element的引用设为null,确保正常回收占用的内存
       element = null;
   }

使用闭包封装变量

假设有一个计算乘积的简单函数

   var mult = function(){
      var a = 1;
      for(var i = 0; i < arguments.length; i++){
          a = a * arguments[i];
      }
      return a;
   }

对于那些相同的参数来说,可以使用缓存来提高效率

   var cache = {};
   var mult = function(){
       //mult(1, 2, 3) => '1, 2, 3'
       var args = array.prototype.join.call(arguments, ',');
       if(cache[args]){
           //使用cache.args会把args自动转成字符串
           return cache[args];
       }
       var a = 1;
       for(var i = 0; i < arguments.length; i++){
          a = a * arguments[i];
       }
       return a;
   }

与其让cache暴露在全局,不如将它封装在iife中

   var mult = (function(){
       var cache = {};
       return function(){
           var args = array.prototype.join.call(arguments, ',');
           if(cache[args]){
               //使用cache.args会把args自动转成字符串
               return cache[args];
           }
           var a = 1;
           for(var i = 0; i < arguments.length; i++){
              a = a * arguments[i];
           }
           return a; 
      }
   })();

提炼函数是代码重构中的一种常见技巧,如果在一个大函数中有一些代码块能够提炼出来,我们常常把这些代码块封装在独立的小函数里面,独立出来的小函数有助于代码复用,如果这些小函数有好的命名,它们本身页起到了注释的作用

   var mult = (function(){
      var cache = {};
      var calculate = function(){
          var a = 1;
      for(var i = 0; i < arguments.length; i++){
           a = a * arguments[i];
      }
      return a; 
      }
      return function(){
         var args = array.prototype.join.call(arguments, ',');
         if(cache[args]){
            return cache[args];
         }
         //将参数传入
         return cache[args] = caculate.apply(null, arguments);
      }
   })()

延续局部变量的寿命

img对象经常用于数据上报

   var report = function(src){
       var img = new image();
       img.src = src;
   };
   report('https://xxx.com/getuserinfo');

而在一些低版本中,report函数并不是每一次都成功发起了http请求,原因是img是局部变量,函数结束调用后就被销毁,可能还没来得及发出http请求

   var report = (function(){
       var imgs = [];
       return function(){
          var img = new image();
          //将img放进闭包变量中
          imgs.push(img);
          img.src = src;    
       }
   })()

用闭包实现命令模式

在完成闭包实现的命令模式之前,我们先用面向对象的方式来编写一段命令模式的代码

   <html>
       <body>
          <button id="execute">点击我执行命令</button>
          <button id="undo">点击我执行命令</button>
       </body>
   </html>
   <script>
       var tv = {
           open: function(){
              console.log('打开电视机');
           },
           close: function(){
              console.log('关上电视机');
           }
       };
       var opentvcommand = function(receiver){
           this.receiver = receiver;
       };
       opentvcommand.prototype.execute = function(){
           this.receiver.open(); //执行命令,打开电视机
       }
       opentvcommand.prototype.undo = function(){
           this.receiver.close(); //撤销命令,关闭电视机
       }
       var setcommand = function(command){
           document.getelementbyid('exucute').onclick = function(){
              command.execute();
           }
           document.getelementbyid('undo').onclick = function(){
              command.undo(); 
           }
       }
       setcommand(new opentvcommand(tv));
   </script>

命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的接收者之间的耦合关系。在命令执行之前,可以预先往命令对象中植入命令的接收者。在闭包的模式中,命令接收者会被封闭在闭包形成的环境中

   var tv = {
      open: function(){
         console.log('打开电视机');
      },
      close: function(){
         console.log('关上电视机');
      }
   };
   var createcommand = function(receiver){
       var execute = function(){
          return receiver.open(); //执行命令,打开电视机
       }
       var undo = function(){
          return receiver.close(); //执行命令,关闭电视机
       }
       return {
          execute: execute,
          undo: undo
       }
   }
   var setcommand = function(command){
           document.getelementbyid('exucute').onclick = function(){
              command.execute();
           }
           document.getelementbyid('undo').onclick = function(){
              command.undo(); 
           }
       }
       setcommand(createcommand(tv));

执行上下文

讲完实际应用之后,下面来看一下高能的理论原理。
执行上下文是ecmascript标准中定义的一个抽象概念,用来记录代码的运行环境。它可以是代码最开始执行的全局上下文,也可以是执行某个函数体内的上下文。
需要注意的是,程序至始至终只能进入一个执行上下文,这就是为什么js是单线程的原因,即每次只能有一个命令在执行。浏览器用栈来维护执行上下文,当前起作用的执行上下文位于栈顶,当它内部的代码执行完毕之后出栈,然后将下一个元素作为当前的上下文。
然而,程序并不需要执行完上下文中的所有代码,才能进入另一个执行上下文(在一个函数中调用另一个函数)。经常有当前的执行上下文a执行到一半暂停,又进入另一个执行上下文的情况。每次一个上下文被另一个上下文替代的时,这个新的上下文就入栈称为栈顶。
当有一堆上下文,有些执行到一半暂停的时候又继续,当继续执行的时候我们需要一种方式去记住当前的状态,事实上ecmascript中已经做出了规定,每个执行上下文都有用来追踪执行状态的记录器

代码执行状态(code evaluation state)在当前执行上下文中用来记录代码执行,暂停,重新执行的状态 函数(function):当前上下文正在执行的函数体 范畴(realm):内部对象集合,全局运行环境极其作用域下的所有代码,其他相关的状态、资源 词法环境(lexical environment):用来解决当前上下文中的标识符引用问题 变量环境(variable environment):包含环境记录(environmentrecord)的词法环境,而环境变量是由变量声明(variablestatements)所产生的

词法环境

用来定义标识符的值:词法环境的目的就是管理代码中的数据。也就是说,它给标识符赋值,让标识符变得有意义。比如,代码段console.log(x/10),如果变量x没有具体值,它是没有意义的,这段代码也没有意义。词法环境通过环境记录将标识符和具体的值联系在一起(见下一点)。 词法环境包含环境记录:环境记录完美地记录了词法环境中所有标识符和具体值之间的联系,并且每个词法环境都有自己的环境记录。 词法嵌套结构:内部环境引用包含它的外部环境,外部环境还可以有自己的外部环境。因此,一个环境可以作为多个内部环境的外部环境。全局环境是唯一一个没有外部环境的环境。

回到闭包

每个函数都有一个包含词法环境的执行上下文,它的词法环境确定了函数内的变量赋值以及对外部环境的引用。看上去函数“记住”了外部环境,但其实上是这个函数有个指向外部环境的引用。这就是“闭包”的概念。

每当外部封闭函数执行的时候就产生了闭包,也就是说闭包的创建并不一定需要内部函数返回。

javascript中闭包作用域是词法作用域,即它在代码写好之后就被静态决定了它的作用域。

        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
            </p

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网