闭包问题

下面的这些内容是翻译了《javascript权威指南》的部分内容:


4.6 变量vs属性
或者你现在已经意识到,在jvascript中变量和对象的属性有很多的相同点。它们有相同的赋值方式,在表达中的使用方式也相同,等等。那么一个变量i和一个对象o的属性i有本质的区别吗?回答是没有。在javascript中变量和对象属性在本质上是相同的。
4.6.1全局对象
当java解释器启动时,在执行javascript代码之前,它做的第一件事情是创建一个全局对象。这个全局对象的属性便是那些个程序的全局变量。当你定义一个全局变量就是给这个全局对象定义一个属性。
javascript解释器使用一系列的内置值和函数来初使化全局对象的属性。例如将Infinity, parseInt,Math属性关联到数字的极限值,内置函数parseInt和内置对象Math。
在顶层域中,你可以使用关键字this来引用这个全局对象。在函数内,this会有不同的用途。
在客户端javascript中,这个全局对象是窗口对象,你可以使用window这个关键字而不是this来引用这个全局对象。
4.6.2局部变量:调用对象
如果全局变量是这个特殊的全局对象的属性,那么局部变量呢?它们同样是对象的属性。这个对象被称为调用对象。调用对象的生命期比全局对象要 短,但它们提供的功能是相同的。当函数体执行时,函数的参数和局部变量做为属性存在调用对象中。这样做是为了防止局部变量与全局变量产生名字冲突。
4.6.3javasciript的执行环境
每次当javascript要执行一个函数时,它会为这个函数创建一个新的执行环境。所谓执行环境,就是说在这个执行环境中的所有 javascript代码都将被执行。执行环境的一个重要组成部分是定义了那些变量的对象。所以,那些不在任何函数中的javascript代码是运行在 将全局对象作为属性的执行环境中。每个函数都运行在自己唯一的运行环境中,这个运行环境拥有自己的调用对象。
需要指出的一点是javascript的实现允许多个全局运行环境的存在,它们各自拥有不同的全局对象。(尽管如此,在这种情况下,每个全局对象并不完全是全局的)。一个很明显的例子是多个浏览器窗口同时开启,或者在一个浏览中存在着多个框架。
4.7 变量作用域
当我们第一次讨论变量的作用域,我们是基于作用域:全局变量拥有全局作用域,局部变量拥有局部作用域。如果一个函数定义在另一个函数之内,变量被定义在内层函数体内。


下面的部分为转贴内容,同样是对《javascript权威指南》的翻译,顺便说一句,这是本好书,呵呵
8.8. 函数作用域与闭包
        如第四章所述,JavaScript函数的函数体在局部作用域中执行,局部作用域不同于全局作用域.本章将解释这些内容和相关的作用域问题,包括闭包.
  • 本章包含超前的内容,如果你是第一次阅读,可以跳过.
    8.8.1. 词法作用域(Lexical Scoping)
            JavaScript中的函数是基于词法作用域的,而不是动态作用域.这句话的意思是JavaScript中的函数运行在它们被定义的作用域里,而不是它 们被执行的作用域里.定义一个函数时,当前作用域链被保存起来并成为该函数内部状态的一部分.作用域链的顶层(最初一层)是由全局对象构成的,这和词法作 用域没什么明显的关联.然而,当你定义一个嵌套函数时,作用域链将包含外层函数(嵌套函数的外层函数.原文:the containing function).这就意味着,被嵌套的函数可以访问外层函数的所有参数和局部变量.

            注意:尽管在一个函数定义的时候,作用域链就已经固定了,但是作用域链中定义的属性并不是固定的.作用域链是"活的"("live"),当函数被调用的时候,它有权访问任何当前被关联的数据.

    8.8.2. 调用对象(The Call Object)
            当JavaScript解释器调用函数的时候,首先,它把作用域设置到作用域链,在函数被定义的时候,该作用域链已经有效.接下来,解释器添加一个叫做调 用对象(ECMAScript规范使用术语:activation object,活动对象)的对象到作用域链的头部.引用Arguments对象的arguments属性为函数初始化调用对象.接下来,添加函数的命名参 数到调用对象.所有用var语句定义的局部变量也都在这个对象中定义.因为调用对象在作用域链的头部,局部变量,函数参数和参数对象都在函数的作用域内. 也就是说它们隐藏了所有同名的在更早的作用域中定义的属性.

    注意:与arguments不同,this是关键字,而不是调用对象的一个属性.

    8.8.3. 调用对象作为命名空间(The Call Object as a Namespace)
            有时,用定义一个简单函数的方法创建一个调用对象是很有用的,这个调用对象可以扮演一个临时命名空间的角色,如此一来,你定义的变量和创建的属性都不会破 坏全局命名空间.例如:假设你有一个Javascrip代码文件,你希望把它用到很多不同的Javascript程序中(或者,用于客户端 Javascript,在很多不同的web页上).假设这些代码像其它代码一样定义了中间变量来保存计算结果.现在的问题是因为这些代码将用于很多不同的 程序,你无法知道此变量是否和其它引入该文件的程序的变量相冲突.

            解决的方法是把代码放到函数里,然后调用这个函数.如此一来,变量是被定义在函数的调用对象中:

    function init() {
         // 代码从这里开始
          // 任何变量声明都会成为调用对象的属性
          // 如此不会破坏全局命名空间.
    }
    init();   // 不要忘了调用这个函数哦!
            这段代码只给全局命名空间添加了一个"init"属性,该属性引用init函数.如果定义一个函数还嫌太多,那么你可以用一个表达式定义和调用一个匿名函数.像这样的JavaScript语法如下:

    ( function () {   // 这个函数没有名字.
          // 代码从这里开始
          // 任何变量声明都会成为调用对象的属性
          // 如此不会破坏全局命名空间.
    } )();           // 结束函数直接量,并调用该函数.

            注意:函数直接量外面的括号是JavaScript语法所必需的.

    8.8.4. 嵌套函数作为闭包(Nested Functions as Closures)
            JavaScript允许函数嵌套,允许把函数作为数据,允许使用词法作用域,把这些结合使用能创造出功能强大的令人惊奇的效果.让我们开始探索,考虑一 下函数g被定义在函数f中.当f被调用的时候,作用域链由为函数f调用生成的调用对象跟随在全局对象之后构成.g函数被定义在f函数里,因此,这个作用域 链作为g函数定义的一部分被保存起来.当g函数被调用的时候,作用域链包括三个部分:g函数自己的调用对象,f函数的调用对象和全局对象.

            嵌套函数在相同的它们被定义的词法作用域里被调用的时候是很容易理解的.例如,下面的代码并没有什么特别:

    var x =   " global " ;
    function f() {
         var x =   " local " ;
         function g() { alert(x); }
        g();
    }
    f();   // 调用这个函数显示 "local"
            然而,在JavaScript中,函数可以像其它值一样作为数据,因此可以在函数中返回一个函数,赋值给对象的属性,存储在数组中等等.这也没有什么特别 的,除了嵌套的函数被调用的时候.考虑下面的代码,它包含一个返回嵌套函数的函数.每次被调用的时候,它都返回一个函数.被返回的函数的 JavaScript代码总是相同的,但是,因为每次调用外层函数时的参数不同,每次被调用的时候,它(被返回的嵌套函数)创建的作用域也有些许不同. (也就是说,对于外层函数的每次调用,都会在作用域链中产生一个不同的调用对象.)如果你把返回函数保存在数组中,然后每一个调用一次,你将发现每一个函 数都返回不同的值.因为每一个函数都由相同的JavaScript代码构成,并且每一次都是从相同的作用域中调用,所以,唯一能造成返回值不同的因素就是 函数被定义的作用域:



    // 每次调用这个函数的时候返回一个函数
    // 函数被定义的作用域在每次调用时都不同
    function makefunc(x) {
         return   function () { return x; }
    }

    // 调用几次 makefunc() , 把结果保存到数组中:
    var a = [makefunc( 0 ), makefunc( 1 ), makefunc( 2 )];

    // 现在调用这些函数并显示结果.
    // 尽管函数体是相同的,但是作用域是不同的,所以每次调用返回不同的结果:
    alert(a[ 0 ]());   // Displays 0
    alert(a[ 1 ]());   // Displays 1
    alert(a[ 2 ]());   // Displays 2
            这段代码的结果是正确的,是根据词法作用域规则的严谨的应用所期待的:函数被执行在它被定义的作用域内.然而,这些结果令人吃惊的原因是,你期待的局部作 用域在定义它们的函数退出的时候就不存在了.事实上,这是正常现象.当函数被调用的时候,解释器创建一个调用对象并把它放到作用域链的头部.当函数退出的 时候,解释器从作用域链上删除这个调用对象.在没有嵌套函数被定义的时候,调用对象是唯一引用作用域链的对象.当调用对象从作用域链上删除时,就再也没有 对它的引用了,它将被GC(garbage collected)回收.

            但是,嵌套函数改变了这些.如果嵌套函数被创建,这个函数的定义引用调用对象,因为这个调用对象是函数被定义的作用域链的顶部.如果嵌套函数只是被外层函 数使用,对嵌套函数的唯一引用在调用对象里.当外层函数返回时,只有嵌套函数引用调用对象,调用对象引用嵌套函数,除此之外,再也没有其它的什么引用任何 一个,因此,这两个对象就只能被GC使用了.

            如果你保存了一个嵌套函数的引用到全局作用域,情况就有所不同了.你把嵌套函数作为外层函数的返回值,或者把嵌套函数保存为其它对象的属性.在这种情况 下,就有了一个对嵌套函数的外部引用,所以,嵌套函数在它的外部函数的调用对象中保持着它的引用.结果是,为外层函数调用生成的调用对象仍然有效,外层函 数的参数和变量的名字和值也保留在这个调用对象里.JavaScript代码无法直接访问调用对象,但是,它定义的作为作用域链的一部分的属性仍用于嵌套 函数的任何调用.(注意:如果外层函数保存了两个嵌套函数的全局引用,那么就有两个嵌套函数共享同一个调用对象,通过调用一个函数对调用对象的改变对另一 个嵌套函数是可见的)

            JavaScript函数是被执行的代码和执行它们的作用域的组合.这个代码和作用域的组合在计算机科学著作中被称作:闭包(closure).所有的 JavaScript函数都是闭包.然而,这些闭包只在象上面讨论的那样时才有趣:当一个嵌套的函数被输出到它被定义的作用域之外.只有嵌套函数被如此使 用时,才被明确的称为闭包.

            闭包是有趣并且功能强大的技术.尽管它们不会被普通的使用在日常JavaScript编程中,它仍然值得我们去理解.如果你理解闭包,你理解作用域链和函数调用对象,那么,你才能真正的称自己为高级JavaScript程序员(JSer ).

    8.8.4.1. 闭包的例子(Closure examples)
            有时,你会想写一个函数,希望它能跨调用保存一个值.这个值不能保存在局部变量里,因为调用对象不会跨调用存在.全局变量是可以的,但是它会破坏全局命名 空间.在8.6.3.章节中,我展现了一个名为uniqueInteger()的函数,它用一个属性保存这个恒久的值.你可以用闭包更进一步实现,创建一 个恒久的私有的变量.下面是不用闭包写的一个函数:


    // 每次调用返回一个不同的整数
    uniqueID =   function () {
         if ( ! arguments.callee.id) arguments.callee.id =   0 ;
         return arguments.callee.id ++ ;
    } ;
            这种方法的问题在于任何人都能设置这个uniqueID.id为0,而破坏了该函数不能返回同一个值两次的约定.你可以通过保存这个恒久值到一个只有你自己的函数有权访问的闭包里的方法来防止别人设置:

    uniqueID = ( function () {   // 这个函数的调用对象保存值
          var id =   0 ;            // 这是私有恒久的那个值
          // 外层函数返回一个有权访问恒久值的嵌套的函数
          // 那就是我们保存在变量uniqueID里的嵌套函数.
          return   function () { return id ++ ; } ;   // 返回,自加.
    } )(); // 在定义后调用外层函数.
            例子8-6是第二个闭包的例子.它示范的是像第一个一样的私有恒久变量,但是这个能被多个函数共享.

            Example 8-6. Private properties with closures

    // 这个函数为对象o的指定名称的属性添加了访问方法
    // 方法名为:get<name>和set<name>.
    // 如果提供了一个判断函数,setter方法将在保存前判断参数是不是有效的
    // 如果检验失败,setter方法抛出一个异常
    // 这个函数的与众不同之处在于,用getter和setter方法操作的属性值并不是存储在对象o里面,
    // 相反的,值被存储在函数的局部变量里.
    // getter和setter方法也被定义为函数的局部方法,因此有权访问这个局部变量.
    // 注意:对于两个访问方法,该值是私有的,除了setter方法,无法修改或设置它.
    function makeProperty(o, name, predicate) {
         var value;   // This is the property value

         // getter方法只是简单的返回值.
         o[ " get "   + name] =   function () { return value; } ;

         // setter保存值,如果校验失败则抛出异常
         o[ " set "   + name] =   function (v) {
             if (predicate &&   ! predicate(v))
                 throw   " set "   + name +   " : invalid value "   + v;
             else
                value = v;
        } ;
    }

    // 下面的代码演示makeProperty() 方法.
    var o =   {} ;   // 这是一个空对象

    // 添加属性访问方法getName() 和 setName()
    // 确保只允许字符串值
    makeProperty(o, " Name " , function (x) { return   typeof x ==   " string " ; } );

    o.setName( " Frank " );   // 设置属性值
    print(o.getName());   // 获得属性值
    o.setName( 0 );         // 试图设置错误类型的值
            我知道的最简单最有用的使用闭包的例子是Steve Yen创建的断点程序,它发布在 http://trimpath.com ,是TrimPath客户端框架的一部分.断点是函数内的一个点,代码执行到该点停止,给程序员检查变量,表达式,调用函数等的值的机会.Steve的断 点技术用闭包捕捉函数的当前作用域(包括局部变量和函数参数),用全局的eval()函数组合这些就可以检查作用域了.eval()函数计算 JavaScript代码字符串并返回结果.下面是一个以自检闭包方式工作的嵌套函数.


    // 捕捉当前作用域,可以用eval()检查
    var inspector = function($) { return eval($); }

            这个函数用了很少见的标识符$作为参数名,这样可以减少在计划检查的作用域内命名冲突的可能性.

            (接下来部分代码与所述内容无关,译略)

    8.8.4.2. 闭包和IE中的内存泄露(Closures and memory leaks in Internet Explorer)
            MS的IE浏览器在ActiveX对象和客户端DOM元素的GC方面表现较弱.客户端对象按引用计数,当引用数为0的时候释放对象.这种方法在循环引用的 时候就失效了,例如,当一个核心JavaScript对象引用一个文档元素,而那个文档元素又有一个属性(比如是一个事件句柄)引用该核心 JavaScript对象.

            在IE客户端编程使用闭包的时候,这种循环引用经常出现.当你使用闭包的时候,记住,封闭(enclosing)函数的调用对象,包括函数所有的参数和局 部变量,都将和闭包一样"长寿".如果任何函数参数或者局部变量引用了一个客户端对象,就会发生内存泄露.
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值