闭包这东西,说难也难,说不难也不难,下面我就以自己的理解来说一下闭包
一、闭包的解释说明
对于函数式语言来说,函数可以保存内部的数据状态。对于像C#这种编译型命令式语言来说,由于代码总是在代码段中执行,而代码段是只读的,因此函数中的数据只能是静态数据。函数内部的局部变量存放在栈上,在函数执行结束以后,所占用的栈被释放,因此局部变量是不能保存的。
Javascript采用词法作用域,函数的执行依赖于变量作用域,这个作用域是在定义函数时确定的。因此Javascript中函数对象不仅保存代码逻辑,还必须引用当前的作用域链。Javascript中函数内部的局部变量可以被修改,而且当再次进入到函数内部的时候,上次被修改的状态仍然持续。这是因为因为局部变量并不保存在栈上,而是通过一个对象来保存。
决定使用哪个变量是由作用域链决定的,每次生成函数实例时,都会为之创建一个对象用来保存局部变量,并且把这个用于保存局部变量的对象加入作用域链中。不同函数对象可以通过作用域链关联起来。Javascript中所有函数都是闭包,我们不能避免“产生”闭包。
引用一张《Javascript高级程序设计》中的图来说明,虽然这张图并不完全说明所有情况。图中的activation object就是用于保存变量的对象。
简而言之,在Javascript中:
闭包:函数实例保存着在执行时所需要的变量的引用,而不会复制保存当时变量的值。(在Object C的实现中,我们可以选择保存当时的值或者是引用)
作用域链:解析变量时查找变量所在的方式,以var作为终止符号,如果链上一直没有var,则一直追溯到全局对象为止。
C#中的闭包特性是由编译器把局部变量转换成引用类型的对象成员实现的。
二、闭包的使用
下面通过一些具体例子来说明如何利用闭包这一特性:
1.闭包是在定义的时候产生的
function Foo(){ function A(){} function B(){} function C(){}}
我们每次执行Foo()的时候,都有有A,B,C这三个函数实例(闭包)产生,当Foo执行完毕,生成的实例没有其他引用,因此会被当成垃圾随之销毁(不一定是马上销毁)。
我们来证实一下作用域链是在函数定义时确定的,所以这里显示的应该是'local scope'
var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() { return scope; } return f;}checkscope()()
同样道理:
(function(){ function A(){} function B(){} function C(){}}())
上面的表达式执行完后也会有A,B,C这三个函数实例(闭包)产生,因为这是一个立即执行的匿名函数,这三个闭包只能产生一次。生成的闭包没有其他引用,因此会被当成垃圾随之销毁(不一定是马上销毁)。
我们之所以这么写,目地有两个
1.避免污染全局对象
2.避免多次产生相同的函数实例
对比下面两个例子,闭包是如何保存作用域链的:
function A(){} //比较省内存的写法,创建对象速度快,开销小 (function(prototype){ var name = "a"; function sayName () { alert(name); } function ChangeName() { name += "_changed" } prototype.sayName = sayName;//引用通过执行匿名函数产生的闭包,闭包只会产生一次 prototype.changeName = ChangeName; }(A.prototype)) var a1 = new A(); var a2 = new A();
a1.sayName(); a1.changeName(); a2.sayName();
--------------------------------------------------------------------------------
function B(){ //프로토타입 체인이 상대적으로 짧고 메서드를 빨리 찾을 수 있지만 new가 생성자를 호출할 때마다 2개의 함수 인스턴스와 1개의 변수가 생성됩니다. var name = "b"; function sayName () { Alert(name); } functionchangeName() { name = "_changed" } this.sayName = sayName;//참조 클로저, 함수 B가 호출될 때마다 Closure of this.changeName =changeName; }//함수 호출 앞에 new 키워드가 있으면 해당 함수가 생성자로 사용됩니다. //기본적으로 생성자로 호출하는 것과 일반 함수로 호출하는 것에는 차이가 없습니다. B()가 직접 호출되면 이 객체는 전역 객체에 바인딩되고 새로 생성된 클로저는 이전 클로저를 대체하고 전역 객체의changeName 및 sayName 속성에 할당되므로 이전 클로저는 처리됩니다. 가비지 수집으로. //생성자로 사용되는 경우 new 키워드는 새 개체를 생성하고(이 새 개체를 가리킴) 이 새 개체의 sayName 및changeName 속성을 초기화하므로 생성된 각 클로저는 참조로 인해 유지됩니다. var b1 = new B(); b1.changeName(); b1.sayName(); b2.sayName();
3. 누출 문제: 컴파일된 언어에서 함수 본문은 항상 파일의 코드 세그먼트에 있으며 런타임 중에 실행 가능한 것으로 표시된 메모리 영역에 로드됩니다. 사실 우리는 함수 자체에 생명주기가 있다고 생각하지 않습니다. 대부분의 경우 "참조 유형 데이터 구조"는 포인터, 객체 등과 같은 수명 주기 및 누수 문제가 있는 것으로 생각합니다.
JavaScript에서 메모리 누수의 본질은 지역 변수를 담는 함수를 정의할 때 생성된 객체가 참조가 존재하기 때문에 가비지로 처리되어 수집되지 않는다는 것입니다.
2. IE6의 DOM 메모리 누수와 같은 일부 객체는 절대 소멸되지 않거나, 소멸 시 Javascript 엔진에 알릴 수 없으므로 일부 Javascript 클로저는 절대 소멸되지 않습니다. 이러한 상황은 일반적으로 Javascript 호스트 개체와 Javascript의 기본 개체 간의 잘못된 통신으로 인해 발생합니다.