《JS权威指南学习总结--8.6 函数闭包》

内容要点:

  和其他大多数现代编程一样,JS也采用词法作用域,也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。

  为了实现这种词法作用域,JS函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。

  闭包概念:函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为"闭包"。

  从技术的角度讲,所有的JS函数都是闭包:它们都是对象,它们都关联到作用域链。定义大多数函数时的作用域链在调用函数时依然有效,但这并不影响闭包。当调用函数时闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域时,事情就变得非常微妙。

 当一个函数嵌套了另外一个函数,外部函数将嵌套的函数对象作为返回值返回的时候往往会发生这种事情。有很多强大的编程技术都利用到了这类嵌套的函数闭包,以至于这种编程模式在JS中非常常见。

一.理解闭包

    理解闭包首先要了解嵌套函数的词法作用域规则。例:

      var scope = "global scope";  //全局变量

     function checkscope(){

          var scope = "local scope";   //局部变量

          function f(){ return scope; } //在作用域中返回这个值

         return f();

      }

     checkscope()     //=>"local scope"

     checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。

    将上面的代码进行一点改动,看代码返回:

     var scope = "global scope"; //全局变量

     function checkscope(){

          var scope = "local scope";   //局部变量

          function f(){ return scope; } //在作用域中返回这个值

          return f;

      }

     checkscope()()     //=>"local scope"

     在这段代码中,checkscope()现在仅仅返回函数内嵌套的一个函数对象,而不是直接返回结果。

     在定义函数的作用域外面,调用这个嵌套的函数(包括最后一行代码的最后一对圆括号):

        回想一下词法作用域的基本规则:JS函数的执行用到了作用域链,这个作用域是函数定义的时候创建的。嵌套的函数f()在这个作用域链里,其中的变量scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回 "local scope" 而不是 "global scope"。

     简而言之,闭包的这个特性强大到让人吃惊:它们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。

二.实现闭包

    如果你理解了词法作用域的规则,你就能很容易地理解闭包: 函数定义时的作用域链到函数执行时依然有效。   然而很多程序员觉得闭包非常难理解,因为他们觉得在外部函数中定义的局部变量在函数返回后就不存在了(这种想法是因为很多人认为函数执行结束后,与之相关的作用域链似乎也不存在了,但在JS中并非如此)。

     如何定义作用域链的:我们将作用域链描述为一个对象列表,不是绑定的栈。每次调用JS函数的时候,都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。

        当函数返回的时候,就从作用域链中将这个 绑定变量的对象 删除。如果不存在嵌套的函数,也没有其他引用指向这个 绑定对象,它就会被当作垃圾回收掉。

        如果定义了嵌套的函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个 变量绑定对象。

        但如果这些嵌套的函数对象在外部函数中保存下来,那么它们也会和所指向的 变量绑定对象 一样当作垃圾回收。

        但如果这个函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当作垃圾回收,并且他所指向的 变量绑定对象 也不会被当作垃圾回收。

三.闭包实现的几个例子

     例子1:

     在8.4自定义函数属性中,定义了uniqueInteger()函数,这个函数使用自身的一个属性来保存每次返回的值,以便每次调用都能跟踪上次的返回值,但这种做法有一个问题,就是恶意代码可能将计数器重置或者把一个非整数赋值给它,导致uniqueInteger()函数不一定能产生"唯一"的 "整数"。

     而闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用做私有状态。可以利用闭包来重写 uniqueInteger()函数:

       var uniqueInteger = (function(){  //定义函数并立即调用

                 var counter = 0;

                 return function(){ return counter++;}

        }());

        console.log(uniqueInteger);  //function
        console.log(uniqueInteger()); //0
        console.log(uniqueInteger()); //1
        console.log(uniqueInteger()); //2

      代码分析:

        粗略来看,第一行代码看起来想将函数赋值给一个变量uniqueInteger,实际上,这段代码定义了一个立即调用的函数,因此是这个函数的返回值赋值给变量uniqueInteger。

        这个函数返回另外一个函数,这是一个嵌套的函数,我们将它赋值给变量uniqueInteger,嵌套的函数是可以访问作用域内的变量的,而且可以访问外部函数中定义的counter变量。

        当外部函数返回之后,其他任何代码都无法访问counter变量,只有内部的函数才能访问到它。

    例子2:

       像counter一样的私有属性不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链:

       function counter(){

          var n = 0;

          return {

             count : function(){ return n++; },

             reset : function(){ n = 0; }

          };

        }

         var c = counter(), d = counter(); //创建两个计数器

         c.count()     //=>0

         d.count()     //=>0

         c.reset()      //reset()和count() 方法共享状态

         c.count()     //=>0: 因为我们重置了c

         d.count()    //=>1:而没有重置d

       代码分析:

          counter()函数返回了一个 "计数器" 对象,这个对象包含两个方法:count()返回下一个整数,reset()将计数器重置为内部状态。

          首先要理解,这两个方法都可以访问私有变量n。再者,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。

          因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另一个对象。

   例子3:

       从技术角度看,可以将这个闭包合并为属性存取器方法getter和setter。下面这个代码所示的counter()函数的版本是6.6节中代码的变种,所不同的是,这里私有状态的实现是利用了闭包,而不是利用普通的对象属性来实现:

       function counter(n){            //函数参数n是一个私有变量

            return {

                 //属性getter方法返回并给私有计数器var递增1

                  get count(){ return n++; },

                  //属性setter不允许n递减

                  set count(m){ if(m >=n ) n = m; else throw Error("count can only be set a larger value"); }

              };

          }

        var c = counter(1000);

        c.count  //=>1000

        c.count  //=>1001

        c.count = 2000

        c.count //=>2001

        c.count = 2000 //=>Error!

       代码分析:

       需要注意的是,这个版本的counter()函数并未声明局部变量,而只是使用参数n来保存私有状态,属性存取器方法可以访问n。这样的话,调用counter()的函数就可以指定私有变量的初始值了。

    例子4:

       使用闭包技术来共享的私有状态的通用做法。这个例子定义了addPrivateProperty()函数,这个函数定义了一个私有变量,以及两个嵌套的函数用来获取和设置这个私有变量的值。

       例:利用闭包实现的私有属性存取器方法

           //这个函数给对象o增加了属性存取器方法,方法名称为get<name>和set<name>。

          //如果提供了一个判定函数setter方法就会用它来检测参数的合法性,然后在存储它

          //如果判定函数返回false,setter方法抛出一个异常

          //

         //这个函数有一个非同寻常之处,就是getter和setter函数,所操作的属性值并没有存储在对象o中。相反,这个值仅仅是保存在函数中的局部变量中

         //getter和setter方法同样是局部函数,因此可以访问这个局部变量

         //也就是说,对于两个存取器方法来说这个变量是私有的,没有办法绕过存取器方法来设置或修改这个值

        function addPrivateProperty(o,name,predicate){

           var value; //这是一个属性值

           //getter方法简单地将其返回

           o["get"+name] = function(){ return value; };

           //setter方法首先检查值是否合法,若不合法就抛出异常,否则就将其存储起来

            o["set"+name] = function(v){ 

                    if (predicate && !predicate(v))

                         throw Error("set"+name+": invalid value" +v);

                    else

                         value = v;  

                  };

               }

      //addPrivateProperty()方法

        var o = {}; //设置了一个空对象

        //增加属性存储器方法getName()和setName()

        //确保只允许字符串值

        addPrivateProperty(o,"Name",function(x){ return typeof x=="string"; });

         o.setName("Frank"); //设置属性值

         console.log(o.getName()); //得到属性值

         o.setName(0);      //试图设置一个错误类型的值

   例子5:

       在同一个作用域链中定义两个闭包,这两个闭包共享同样的私有变量或变量。这是一种非常重要的技术,但还是要特别小心那些不希望共享的变量往往不经意间共享给了其它的闭包,

       例:

         //这个函数返回一个总是返回v的函数

           function constfunc(v){ return function(){ return v; }; }

        //创建一个数组用来存储常数函数

           var funcs = [];

          for(var i = 0;i<10;i++) funcs[i] = constfunc(i);

          funcs[5]() //=>5

        这段代码利用循环创建了很多个闭包,当写类似这种代码的时候往往会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数之内。

        例:

          //返回一个函数组成的数组,它们的返回值是0-9

           function constfuncs(){

              var funcs = [];

              for(var i = 0 ;i <10;i++)

                    funcs[i] = function(){ return i; };

              return funcs;

            }

           var funcs = constfuncs();

          funcs[5]() //=》10

         当constfuncs()返回时,变量i的是10,所有的闭包都共享这一个值,因此,数组中的函数的返回值都是同一个值,这不是我们想要的结果

         关联到闭包的作用域链都是"活动的",这一点非常重要。

        嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生产静态快照。

    书写闭包需要注意的事情:

          this是js的关键字,而不是变量。正如之前讨论的,每个函数调用都包含一个this值,如果闭包在外包函数里是无法访问this的,除非外部函数将this转存为一个变量:

               var self = this; //将this保存至一个变量中,以便嵌套的函数能够访问它

          绑定arguments的问题与之类似。arguments并不是一个关键字,但在调用每个函数时都会自动声明它,由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中:

          var outerArguments = arguments; //保存起来以便嵌套的函数能使用它

时间: 2024-11-08 18:55:43

《JS权威指南学习总结--8.6 函数闭包》的相关文章

CI框架源码阅读笔记3 全局函数Common.php

从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap引导文件都会最先引入全局函数,以便于之后的处理工作). 打开Common.php中,第一行代码就非常诡异: if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 上一篇(CI框架源码阅读笔记2 一切的入口 index

IOS测试框架之:athrun的InstrumentDriver源码阅读笔记

athrun的InstrumentDriver源码阅读笔记 作者:唯一 athrun是淘宝的开源测试项目,InstrumentDriver是ios端的实现,之前在公司项目中用过这个框架,没有深入了解,现在回来记录下. 官方介绍:http://code.taobao.org/p/athrun/wiki/instrumentDriver/ 优点:这个框架是对UIAutomation的java实现,在代码提示.用例维护方面比UIAutomation强多了,借junit4的光,我们可以通过junit4的

Yii源码阅读笔记 - 日志组件

?使用 Yii框架为开发者提供两个静态方法进行日志记录: Yii::log($message, $level, $category);Yii::trace($message, $category); 两者的区别在于后者依赖于应用开启调试模式,即定义常量YII_DEBUG: defined('YII_DEBUG') or define('YII_DEBUG', true); Yii::log方法的调用需要指定message的level和category.category是格式为“xxx.yyy.z

源码阅读笔记 - 1 MSVC2015中的std::sort

大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格式化,去掉或者展开用于条件编译或者debug检查的宏,依重要程度重新排序函数,但是不会改变命名方式(虽然MSVC的STL命名实在是我不能接受的那种),对于代码块的解释会在代码块前(上面)用注释标明. template<class _RanIt, class _Diff, class _Pr> in

CI框架源码阅读笔记5 基准测试 BenchMark.php

上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功能,各模块之间可以相互调用,共同构成了CI的核心骨架. 从本篇开始,将进一步去分析各组件的实现细节,深入CI核心的黑盒内部(研究之后,其实就应该是白盒了,仅仅对于应用来说,它应该算是黑盒),从而更好的去认识.把握这个框架. 按照惯例,在开始之前,我们贴上CI中不完全的核心组件图: 由于BenchMa

CI框架源码阅读笔记2 一切的入口 index.php

上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里这次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中,我们并不会逐行进行解释,而只解释核心的功能和实现. 1.       设置应用程序环境 define('ENVIRONMENT', 'development'); 这里的development可以是任何你喜欢的环境名称(比如dev,再如test),相对应的,你要在下面的switch case代码块中

Apache Storm源码阅读笔记

欢迎转载,转载请注明出处. 楔子 自从建了Spark交流的QQ群之后,热情加入的同学不少,大家不仅对Spark很热衷对于Storm也是充满好奇.大家都提到一个问题就是有关storm内部实现机理的资料比较少,理解起来非常费劲. 尽管自己也陆续对storm的源码走读发表了一些博文,当时写的时候比较匆忙,有时候衔接的不是太好,此番做了一些整理,主要是针对TridentTopology部分,修改过的内容采用pdf格式发布,方便打印. 文章中有些内容的理解得益于徐明明和fxjwind两位的指点,非常感谢.

CI框架源码阅读笔记4 引导文件CodeIgniter.php

到了这里,终于进入CI框架的核心了.既然是"引导"文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.com/usr/reg 经过引导文件,实际上会交给Application中的UsrController控制器的reg方法去处理. 这之中,CodeIgniter.php做了哪些工作?我们一步步来看. 1.    导入预定义常量.框架环境初始化 之前的一篇博客(CI框架源码阅读笔记2 一切的入

jdk源码阅读笔记之java集合框架(二)(ArrayList)

关于ArrayList的分析,会从且仅从其添加(add)与删除(remove)方法入手. ArrayList类定义: p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 18.0px Monaco } span.s1 { color: #931a68 } public class ArrayList<E> extends AbstractList<E> implements List<E> ArrayList基本属性: /** *

dubbo源码阅读笔记--服务调用时序

上接dubbo源码阅读笔记--暴露服务时序,继续梳理服务调用时序,下图右面红线流程. 整理了调用时序图 分为3步,connect,decode,invoke. 连接 AllChannelHandler.connected(Channel) line: 38 HeartbeatHandler.connected(Channel) line: 47 MultiMessageHandler(AbstractChannelHandlerDelegate).connected(Channel) line: