http://www.felixwoo.com/archives/247
最近在网上查阅了不少Javascript闭包(closure)相关的资料,写的大多是非常的学术和专业。对于初学者来说别说理解闭包了,就连文字叙述都很难看懂。撰写此文的目的就是用最通俗的文字揭开Javascript闭包的真实面目。
“官方”的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
相信很少有人能直接看懂这句话,因为他描述的太学术。其实这句话通俗的来说就是:JavaScript中所有的function都是一个闭包。不过一般来说,嵌套的function所产生的闭包更为强大,也是大部分时候我们所谓的“闭包”。看下面这段代码:
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c();
这段代码有两个特点:
引用关系如图:
这样在执行完var c=a()后,变量c实际上是指向了函数b,b中用到了变量i,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个我们通常所谓的“闭包”。
让我们说的更透彻一些。所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标 对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目 标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新 的值,和上次那次调用的是各自独立的。
为了更深刻的理解闭包,下面让我们继续探索闭包的作用和效果。
(转载请注明出处:http://www.felixwoo.com/archives/247)
简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。这是对闭包作用的非常直白的描述,不专业也不严谨,但你一定能看懂。理解闭包需要循序渐进的过程。
在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。
那么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)
(转载请注明出处:http://www.felixwoo.com/archives/247)
如果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
如图所示,当在函数b中访问一个变量的时候,搜索顺序是:
小结,本段中提到了两个重要的词语:函数的定义与执行。文中提到函数的作用域是在定义函数时候就已经确定,而不是在执行的时候确定(参看步骤1和3)。用一段代码来说明这个问题:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
这段代码中变量h指向了f中的那个匿名函数(由g返回)。
如果第一种假设成立,那输出值就是undefined;如果第二种假设成立,输出值则为1。
运行结果证明了第2个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了。
(转载请注明出处:http://www.felixwoo.com/archives/247)
function Constructor(...) { var that = this; var membername = value; function membername(...) {...} }
以上3点是闭包最基本的应用场景,很多经典案例都源于此。
(转载请注明出处:http://www.felixwoo.com/archives/247)
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
(转载请注明出处:http://www.felixwoo.com/archives/247)
理解JavaScript的闭包是迈向高级JS程序员的必经之路,理解了其解释和运行机制才能写出更为安全和优雅的代码。如果您对本文有任何的建议和疑问,欢迎留言。(转载请注明出处:http://www.felixwoo.com/archives/247)
WIKI中的介绍如下:
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
闭包的概念出现于60年代,最早实现闭包的程序语言是Scheme。之后,闭包被广泛使用于函数式编程语言如ML语言和LISP。很多命令式程序语言也开始支持闭包。
在一些语言中,在函数中定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。
目录[隐藏]
|
Peter J. Landin 在1964年将术语 闭包 定义为一种包含 环境成分 和 控制成分的实体,用于在他的SECD 机器上对表达式求值。[1] Joel Moses 认为是 Landin 发明了 闭包 这一术语,用来指代某些其开放绑定(自由变量)已经由其语法环境完成闭合(或者绑定)的 lambda 表达式,从而形成了 闭合的表达式,或称闭包。[2][3] 这一用法后来于 1975 年被 Sussman 和 Steele 在定义 Scheme 语言的时候予以采纳。[4] 并广为流传。
闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。
不过,用这种方式来使用闭包时,闭包不再具有引用透明性,因此也不再是纯函数。即便如此,在某些“近似于函数式编程语言”的语言,例如Scheme中,闭包还是得到了广泛的使用。
闭包通常出现在将函数当作第一级对象的语言中——在这些语言中,函数可以被当作参数传递,也可以作为返回值返回,就像字符串、整数等简单类型。例如以下Scheme代码:
; Return a list of all books with at least THRESHOLD copies sold. (define (best-selling-books threshold) (filter (lambda (book) (>= (book-sales book) threshold)) book-list))
在这个例子中,lambda表达式(lambda (book) (>= (book-sales book) threshold))
出现在函数best-selling-books
中。当这个lambda表达式被执行时,Scheme创造了一个包含此表达式以及对threshold
变量的引用的闭包,其中threshold
变量在lambda表达式中是自由变量。
这个闭包接着被传递到filter
函数。这个函数的功能是重复调用这个闭包以判断哪些书需要增加到列表那些需要丢弃。因为闭包中引用了变量threshold
,所以它在每次被filter
调用时都可以使用这个变量,虽然filter
可能定义在另一个文件中。
下面是用ECMAScript (JavaScript)写的同一个例子:
// Return a list of all books with at least 'threshold' copies sold. function bestSellingBooks(threshold) { return bookList.filter( function (book) { return book.sales >= threshold; } ); }
这里,关键字function
取代了lambda
,Array.filter
方法[5]取代了filter
函数,但两段代码的功能是一样的。
一个函数可以创建一个闭包并返回它,就像这样:
// Return a function that approximates the derivative of f // using an interval of dx, which should be appropriately small. function derivative(f, dx) { return function (x) { return (f(x + dx) - f(x)) / dx; }; }
因为在这个例子中闭包已经超出了创建它的函数的范围,所以变量f
和dx
将在函数derivative
返回后继续存在。在没有闭包的语言中,变量的生命周期只限于创建它的环境。但在有闭包的语言中,只要有一个闭包引用了这个变量,它就会一直存在。清理不被任何函数引用的变量的工作通常由垃圾回收完成。
(define foo #f) (define bar #f) (let ((secret-message "none")) (set! foo (lambda (msg) (set! secret-message msg))) (set! bar (lambda () secret-message))) (display (bar)) ; prints "none" (newline) (foo "meet me by the docks at midnight") (display (bar)) ; prints "meet me by the docks at midnight"
在C语言中,支持回调函数的库有时在注册时需要两个参数:一个函数指针,一个独立的void*
指针用以保存用户数据。这样的做法允许回调函数恢复其调用时的状态。这样的惯用法在功能上类似于闭包,但语法上有所不同。
一些面向对象的语言包含了一些闭包的特性,例如:
C语言 (使用LLVM编译器或苹果修改版的GCC)支持块。闭包变量用__block标记。同时,这个扩展也可以应用到Objective-C与C++中。
typedef int (^IntBlock)(); IntBlock downCounter(int start) { __block int i = start; return Block_copy( ^int() { return i--; }); } IntBlock f = downCounter(5); printf("%d", f()); printf("%d", f()); printf("%d", f()); Block_release(f);
C++允许通过重载operator()
来定义函数对象。这种对象的行为在某种程度上与函数式编程语言中的函数类似。它们可以在运行时创建,保存状态,但是不能如闭包一般隐式获取局部变量。C++标准委员会正在考虑两种在C++中引入闭包的建议(它们都称为lambda函数)[1], [2]。这些建议间主要的区别在于一种默认在闭包中储存全部局部变量的拷贝,而另一种只存储这些变量的引用。这两种建议都提供了可以覆盖默认行为的选项。若这两种建议之一被接受,则可以写如下代码
void foo(string myname) { typedef vector<string> names; int y; names n; // ... names::iterator i = find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;}); // 'i' is now either 'n.end()' or points to the first string in 'n' // 'i' 现在是'n.end()'或指向'n'中第一个 // 不等于'myname'且长度大于'y'的字符串 }
至少两种C++编译器,Visual C++ 2010(或Visual C++ 10.0)与gcc-4.5已经支持了这种特性。