您好、欢迎来到现金彩票网!
当前位置:秒速快三计划 > 属性闭包 >

JavaScript闭包详解

发布时间:2019-06-07 06:17 来源:未知 编辑:admin

  本文将介绍一个在JavaScript经常会拿来讨论的话题 闭包(closure)。 闭包其实已经是个老生常谈的话题了; 有大量文章都介绍过闭包的内容, 尽管如此,这里还是要试着从理论角度来讨论下闭包, 看看ECMAScript中的闭包内部究竟是如何工作的。

  在讨论ECMAScript闭包之前,先来介绍下函数式编程(与ECMA-262-3 标准无关)中一些基本定义。 然而,为了更好的解释这些定义,这里还是拿ECMAScript来举例。

  众所周知,在函数式语言中(ECMAScript也支持这种风格),函数即是数据。就比方说,函数可以保存在变量中,可以当参数传递给其他函数,还可以当返回值返回等等。 这类函数有特殊的名字和结构。

  上述例子中funArg的实参是一个传递给exampleFunc的匿名函数。

  反过来,接受函数式参数的函数称为 高阶函数(high-order function 简称:HOF)。还可以称作:函数式函数 或者 偏数理的叫法:操作符函数。 上述例子中,exampleFunc 就是这样的函数。

  可以以正常数据形式存在的函数(比方说:当参数传递,接受函数式参数或者以函数值返回)都称作 第一类函数(一般说第一类对象)。 在ECMAScript中,所有的函数都是第一类对象。

  在函数式参数中定义的变量,在“funArg”激活时就能够访问了(因为存储上下文数据的变量对象每次在进入上下文的时候就创建出来了):

  然而,我们知道,在ECMAScript中,函数是可以封装在父函数中的,并可以使用父函数上下文的变量。 这个特性会引发 funArg问题。

  在面向堆栈的编程语言中,函数的本地变量都是保存在堆栈上的, 每当函数激活的时候,这些变量和函数参数都会压栈到该堆栈上。

  当函数返回的时候,这些参数又会从堆栈中移除。这种模型对将函数作为函数式值使用的时候有很大的限制(比方说,作为返回值从父函数中返回)。 绝大部分情况下,问题会出现在当函数有自由变量的时候。

  上述例子中,对于innerFn函数来说,localVar就属于自由变量。

  对于采用 面向堆栈模型来存储局部变量的系统而言,就意味着当testFn函数调用结束后,其局部变量都会从堆栈中移除。 这样一来,当从外部对innerFn进行函数调用的时候,就会发生错误(因为localVar变量已经不存在了)。

  而且,上述例子在 面向堆栈实现模型中,要想将innerFn以返回值返回根本是不可能的。 因为它也是testFn函数的局部变量,也会随着testFn的返回而移除。

  还有一个函数对象问题和当系统采用动态作用域,函数作为函数参数使用的时候有关。

  我们看到,采用动态作用域,变量(标识符)处理是通过动态堆栈来管理的。 因此,自由变量是在当前活跃的动态链中查询的,而不是在函数创建的时候保存起来的静态作用域链中查询的。

  这样就会产生冲突。比方说,即使Z仍然存在(与之前从堆栈中移除变量的例子相反),还是会有这样一个问题: 在不同的函数调用中,Z的值到底取哪个呢(从哪个上下文,哪个作用域中查询)?

  上述描述的就是两类 funArg问题 取决于是否将函数以返回值返回(第一类问题)以及是否将函数当函数参数使用(第二类问题)。

  为了解决上述问题,就引入了 闭包的概念。下面重点来了,闭包的面纱就此揭开。

  上述例子中,“fooClosure”部分是伪代码。对应的,在ECMAScript中,“foo”函数已经有了一个内部属性创建该函数上下文的作用域链。

  这里“lexical”是不言而喻的,通常是省略的。上述例子中是为了强调在闭包创建的同时,上下文的数据就会保存起来。 当下次调用该函数的时候,自由变量就可以在保存的(闭包)上下文中找到了,正如上述代码所示,变量“z”的值总是10。

  定义中我们使用的比较广义的词 “代码块”,然而,通常(在ECMAScript中)会使用我们经常用到的函数。 当然了,并不是所有对闭包的实现都会将闭包和函数绑在一起,比方说,在Ruby语言中,闭包就有可能是: 一个程序对象(procedure object), 一个lambda表达式或者是代码块。

  对于要实现将局部变量在上下文销毁后仍然保存下来,基于堆栈的实现显然是不适用的(因为与基于堆栈的结构相矛盾)。 因此在这种情况下,上层作用域的闭包数据是通过 动态分配内存的方式来实现的(基于“堆”的实现),配合使用垃圾回收器(garbage collector简称GC)和 引用计数(reference counting)。 这种实现方式比基于堆栈的实现性能要低,然而,任何一种实现总是可以优化的: 可以分析函数是否使用了自由变量,函数式参数或者函数式值,然后根据情况来决定 是将数据存放在堆栈中还是堆中。

  讨论完理论部分,接下来让我们来介绍下ECMAScript中闭包究竟是如何实现的。 这里还是有必要再次强调下:ECMAScript只使用静态(词法)作用域(而诸如Perl这样的语言,既可以使用静态作用域也可以使用动态作用域进行变量声明)。

  从技术角度来说,创建该函数的上层上下文的数据是保存在函数的内部属性[[Scope]]中的。如果你对[[Scope]]和作用域链的知识完全理解了的话,那对闭包也就完全理解了。

  根据函数创建的算法,我们看到 在ECMAScript中,所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链(除开异常的情况) (不管这个函数后续是否会激活 [[Scope]]在函数创建的时候就有了):

  正如此前提到过的,出于优化的目的,当函数不使用自由变量的时候,实现层可能就不会保存上层作用域链。 然而,ECMAScript-262-3标准中并未对此作任何说明;因此,严格来说 所有函数都会在创建的时候将上层作用域链保存在[[Scope]]中。

  这里还要注意的是:在ECMAScript中,同一个上下文中创建的闭包是共用一个[[Scope]]属性的。 也就是说,某个闭包对其中的变量做修改会影响到其他闭包对其变量的读取:

  正因为这个特性,很多人都会犯一个非常常见的错误: 当在循环中创建了函数,然后将循环的索引值和每个函数绑定的时候,通常得到的结果不是预期的(预期是希望每个函数都能够获取各自对应的索引值)。这个错误也是题目中说提到的那段代码的最大错误的地方,下面我们来揭秘为啥button点击弹出来的都是5.

  上述例子就证明了 同一个上下文中创建的闭包是共用一个[[Scope]]属性的。因此上层上下文中的变量“k”是可以很容易就被改变的。

  上述例子中,函数“_helper”创建出来之后,通过参数“k”激活。其返回值也是个函数,该函数保存在对应的数组元素中。 这种技术产生了如下效果: 在函数激活时,每次“_helper”都会创建一个新的变量对象,其中含有参数“x”,“x”的值就是传递进来的“k”的值。 这样一来,返回的函数的[[Scope]]就成了如下所示:

  我们看到,这个时候函数的[[Scope]]属性就有了真正想要的值了,为了达到这样的目的,我们不得不在[[Scope]]中创建额外的变量对象。 要注意的是,在返回的函数中,如果要获取“k”的值,那么该值还是会是3。

  顺便提下,大量介绍JavaScript的文章都认为只有额外创建的函数才是闭包,这种说法是错误的(我也差点有这个错误的认识,再次感谢作者指出。)。 实践得出,这种方式是最有效的,然而,从理论角度来说,在ECMAScript中所有的函数都是闭包。

  然而,上述提到的方法并不是唯一的方法。通过其他方式也可以获得正确的“k”的值,如下所示:

  另外一个特性是从闭包中返回。在ECMAScript中,闭包中的返回语句会将控制流返回给调用上下文(调用者)。 而在其他语言中,比如,Ruby,有很多中形式的闭包,相应的处理闭包返回也都不同,下面几种方式都是可能的:可能直接返回给调用者,或者在某些情况下直接从上下文退出。

  然而,在ECMAScript中通过try catch可以实现如下效果(这个方法简直太棒了!):

  通常,程序员会错误的认为,只有匿名函数才是闭包。其实并非如此,正如我们所看到的 正是因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包), 这里只有一类函数除外,那就是通过Function构造器创建的函数,因为其[[Scope]]只包含全局对象。 为了更好的澄清该问题,我们对ECMAScript中的闭包作两个定义(即两种闭包):

  实际使用的时候,闭包可以创建出非常优雅的设计,允许对funArg上定义的多种计算方式进行定制。 如下就是数组排序的例子,它接受一个排序条件函数作为参数:

  同样的例子还有,数组的map方法(并非所有的实现都支持数组map方法,SpiderMonkey从1.6版本开始有支持),该方法根据函数中定义的条件将原数组映射到一个新的数组中:

  使用函数式参数,可以很方便的实现一个搜索方法,并且可以支持无穷多的搜索条件:

  还有应用函数,比如常见的forEach方法,将funArg应用到每个数组元素:

  顺便提下,函数对象的 apply 和 call方法,在函数式编程中也可以用作应用函数。这里,我们将它们看作是应用函数 应用到参数中的函数(在apply中是参数列表,在call中是独立的参数):

  闭包还有另外一个非常重要的应用 延迟调用:

  testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.(再具体原因,前面有解释过。)

  例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。

  执行结果是弹出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。

  这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访 问内部私有函数。需要注意的地方是匿名主函数结束的地方的(),如果没有这个()就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被 其他地方调用。这个就是利用闭包产生单件的方法。

http://isaegil.net/shuxingbibao/188.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有