闭包整理

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的创建方式,就是在一个函数的内部创建另外一个函数。

				createComparisonFunction (propertyName){
					return function (object1,object2) {
						var value1 = object1[propertyName];//*
						var value2 = object1[propertyName];//*
						if (value1 < value2) {
								return -1;
						}else if (value1 > value2) {
								return 1;
						}else {
							return 0;
						}
					}
				}

上面标记星号的二行代码是内部函数(一个匿名函数)中的代码,
这两行代码访问了外部函数中的变量propertyName。即使这个内部函数被
返回了,而且是在其他的地方被调用了,但它仍然可以访问变量propertyName。
之所以还可以访问到这个变量,是因为内部函数的作用域链中包含了createComparisonFunction()的作用域。

当某个函数第一次被调用时,会创建一个执行环境(execution context)及相应的作用链,并把作用域链赋值给
一个特殊的内部属性(即[[Scope]])。然后使用this,arguments和其他命名参数的值来初始化函数的活动对象
(activation object)。但在作用域链中,外部函数的活动对始终处于第二位,外部函数的外部函数的活动对象
处于第三位,.....直到作用域链终点的全局执行环境。

在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。

				function compare (value1,value2){
						if (value1 > value2) {
								return 1;
						}else if (value1 < value2) {
								return -1;
						}else {
							return 0;
						}
				}
				var result = compare (5,10);

上面的代码先定义了compare()函数,然后又在全局作用域中调用了它。当第一个调用compare()时,
会创建一个包含this,arguments,value1,value2的活动对象。
全局执行环境的变量也包含this,result,compare,在compre()执行环境的作用域链中则处于第二位。

图:compare()函数执行时的作用域链。


后台的每个执行环境都有一个表示变量的对象-变量对象。全局环境的变量对象始终存在,而象compare()
函数这样的局部环境变量对象,则只在函数执行的过程中存在。
在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的
[[Scope]]属性中。当调用compare函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]
属性中的对象构建起执行环境的作用域链。此后又有一个活动对象(在此作为变量对象使用)被创建并推入
执行环境作用域链的前端。对于这个例子中的compare函数的执行环境而言,起作用域链中包含二个变量对象;
本地活动对象和全局变量对象。显然,作用域链本质上就是一个指向变量对象的指针列表,它引用但不实际
包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,
当函数执行完毕后,局部活动对象就会销毁,内存中仅仅保存全局作用域(全局执行环境的变量对象)
但是,闭包的情况下就不同了。
在另一个函数内部定义的函数将包含函数的活动对象添加到它的作用域链中。
所以:在createComparisonFunction函数内部定义的匿名函数的作用域链中,实际将会包含外部函数
createComparisonFunction()的活动对象。图:


				var compare = createComparisonFunction ("name");
				var result = compare ({name:"aa"},{name:"b"});

在匿名函数从createComparisonFunction()中被返回后,它的作用域就被
初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。
这样,匿名函数就可以访问在createComparisonFunction()中定义的所有的变量。
更为重要的是,createComparisonFunction()函数执行完毕后,其活动对象也不会销毁,
因为匿名对象的作用域仍然在引用这个活动对象。换句话说:
当createComparisonFunction()函数返回后,其执行环境的作用域会被销毁,但它的活动对象仍然
会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

				//创建函数
				var compareNames = createComparisonFunction("name");
				//调用函数
				var compare = createComparisonFunction ("name");
				//解除对匿名函数的引用(以便释放内存)
				compareNames = null;

通过将compareNames设置为null,解除对函数的引用,就等于通知垃圾回收例程将其清除。
随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也可以安全的销毁了。
上图为调用compareNames()的过程产生的作用域链之间的关系。

闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包保存的是整个变量对象而不是某个特殊的变量。

				function createFunctions () {
					var result = new Array();
					for (var i = 0;i < 10;i++) {
						result[i] = function () {
							return i;
						};
					}
					return result;
				}
				var array = createFunctions ();
				for (var i = 0;i < array.length;i++) {
						 alert (array[i]());//所有的结果都是10
				}
				array = null;

因为每个函数的作用域中都保存着createFunctions()函数的活动对象,所以它们引用的
都是同一个变量i。
我们可以通过创建另外一个匿名函数强制闭包的行为符合预期,

				function createFunctions () {
					var result = new Array ();
					for (var i = 0;i < 10;i++) {
						result[i] = function (num) {
							return function () {
								return num;
							};
						}(i);
					}
					return result;
				}

我们定义一个匿名的函数,并立即执行该匿名函数的结果赋值给数组。
这里的匿名函数有一个参数num,也就是最终的函数要返回的值。
在调用哪个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,
所以就会将变量i的当前复制给参数num。而在这个匿名函数的内部,又
创建并返回了一个访问num的闭包。这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回
各自不同的数值了。
关于this对象
this对象在运行的时候是基于函数执行的环境绑定的:
在全局环境中this等于window,而函数被作为某个对象的方法调用时,
this等于那个对象。不过,匿名函数的执行环境具有全局性,因此this对象通常指向window。
但有的时候由于编写闭包的方式不同,可能不会那么的明显。

				var name = "The Window";
				var object = {
					name:"myObject",
					getNameFunc:function () {
						return function () {
							return this.name;
						};
					}
				};
				alert (object.getNameFunc()());//"The Window"

前面曾经提到过,每个函数在被调用的时候,其活动对象都会自动取得两个特殊的
变量:this和arguments。内部函数在搜索着两个变量时,只会搜索其活动对象为止,
因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象,保存在
一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

				var name = "The Window";
				var object = {
					name:"myObject",
					getNameFunc:function () {
						var that = this;
						return function () {
							return that.name;
						};
					}
				};

内存的泄漏问题
由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程。
具体来说:如果闭包的作用域保存着一个HTML元素,那么就意味着该元素无法被销毁。
eg:
				function assignHandler () {
						var element = document.getElementById("demo");
						element.onclick = function () {
							alert (element.id);	
						};	
					}
					assignHandler();

上面的代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。
由于匿名函保存了一个对assignHandler()活动对象的引用,因此就会导致无法减少element的引用数。
只要匿名函数存在,element的引用数至少为1,因此它所占用的内存就永远不会被回收,不过。这个问题就可以
通过稍微改写一些代码来解决。

					function assignHandler () {
						var element = document.getElementById("demo");
						var id = element.id;
						element.onclick = function () {
							alert (id);	
						};	
						element = null;
					}
					assignHandler();

在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。
但是做到这一步,还是不可以解决内存泄露的问题。必须记住:闭包会引用包含函数的整个活动对象,而其中包含
着element。即使闭包不直接引用element,包含函数的活动对象也仍然会保存一个引用。因此,有必要把element
变为null。这样就能够接触对DOM对象的引用,顺利的减少引用数,确保正常的回收器占用的内存。
模仿块级作用域。
JavaScript是没有块级作用域的概念。

					function outputNumbers (count) {
						for (var i = 0;i < count;i++) {
							//......
						}
						alert(i);//可以访问到 10
					}
					outputNumbers(10);

匿名函数可以用来模仿块级作用域避免这个问题。
用作块级作用据的匿名函数的语法如下所示:

					(function () {
						//这里是块级作用域
					 })();

以上定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示他实际上 是一个函数表达式。 无论什么时候,只要临时需要一些变量,就可以使用私有作用域。
					 function outputNumbers (count) {
					 		(function(){
					 			for (var i = 0;i < count;i++) {
					 					alert (i);
					 			}
					 		})();
					 		alert(i);//error
					 }




update:


模仿块级作用域
JavaScript 没有块级作用域的概念。

	function box(count) {

	for (var i=0; i<count; i++) {}

	alert(i); //i 不会因为离开了for 块就失效

	}

	box(2);

	function box(count) {

	for (var i=0; i<count; i++) {}

	var i; //就算重新声明,也不会前面的值

	alert(i);

	}

	box(2);

以上两个例子,说明JavaScript 没有块级语句的作用域,if () {} for () {}等没有作用域,
如果有,出了这个范围i 就应该被销毁了。就算重新声明同一个变量也不会改变它的值。
JavaScript 不会提醒你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声
明视而不见(如果初始化了,当然还会执行的)。使用模仿块级作用域可避免这个问题。
	//模仿块级作用域(私有作用域)

	(function () {

	//这里是块级作用域

	})();

	//使用块级作用域(私有作用域)改写

	function box(count) {

	(function () {

	for (var i = 0; i<count; i++) {}

	})();

	alert(i); //报错,无法访问

	}

	box(2);

使用了块级作用域(私有作用域)后,匿名函数中定义的任何变量,都会在执行结束时被
销毁。这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的
变量和函数。一般来说,我们都应该尽可能少向全局作用域中添加变量和函数。在大型项目
中,多人开发的时候,过多的全局变量和函数很容易导致命名冲突,引起灾难性的后果。如
果采用块级作用域(私有作用域),每个开发者既可以使用自己的变量,又不必担心搞乱全局
作用域。
	(function () {

	var box = [1,2,3,4];

	alert(box); //box 出来就不认识了

	})();

在全局作用域中使用块级作用域可以减少闭包占用的内存问题,因为没有指向匿名函数
的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
私有变量
JavaScript 没有私有属性的概念;所有的对象属性都是公有的。不过,却有一个私有变
量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问
这些变量。
	function box() {

	var age = 100; //私有变量,外部无法访问

	}

而通过函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而
利用这一点,可以创建用于访问私有变量的公有方法。
	function Box() {

	var age = 100; //私有变量

	function run() { //私有函数

	return '运行中...';

	}

	this.get = function () { //对外公共的特权方法

	return age + run();

	};

	}

	var box = new Box();

	alert(box.get());

可以通过构造方法传参来访问私有变量。
	function Person(value) {

	var user = value; //这句其实可以省略

	this.getUser = function () {

	return user;

	};

	this.setUser = function (value) {

	user = value;

	};

	}

但是对象的方法,在多次调用的时候,会多次创建。可以使用静态私有变量来避免这个
问题。
静态私有变量
通过块级作用域(私有作用域)中定义私有变量或函数,同样可以创建对外公共的特权方
法。
	(function () {

	var age = 100;

	function run() {

	return '运行中...';

	}

	Box = function () {}; //构造方法

	Box.prototype.go = function () { //原型方法

	return age + run();

	};

	})();

	var box = new Box();

	alert(box.go());

上面的对象声明,采用的是Box = function () {} 而不是function Box() {} 因为如果用后
面这种,就变成私有函数了,无法在全局访问到了,所以使用了前面这种。
	(function () {

	var user = '';

	Person = function (value) {

	user = value;

	};

	Person.prototype.getUser = function () {

	return user;

	};

	Person.prototype.setUser = function (value) {

	user = value;

	}

	})();

使用了prototype 导致方法共享了,而user 也就变成静态属性了。(所谓静态属性,即共
享于不同对象中的属性)。
模块模式
之前采用的都是构造函数的方式来创建私有变量和特权方法。那么对象字面量方式就采
用模块模式来创建。
	var box = { //字面量对象,也是单例对象

	age : 100, //这是公有属性,将要改成私有

	run : function () { //这时公有函数,将要改成私有

	return '运行中...';

	};

	};

私有化变量和函数:
	var box = function () {

	var age = 100;

	function run() {

	return '运行中...';

	}

	return { //直接返回对象

	go : function () {

	return age + run();

	}

	};

	}();

上面的直接返回对象的例子,也可以这么写:
	var box = function () {

	var age = 100;

	function run() {

	return '运行中...';

	}

	var obj = { //创建字面量对象

	go : function () {

	return age + run();

	}

	};

	return obj; //返回这个对象

	}();

字面量的对象声明,其实在设计模式中可以看作是一种单例模式,所谓单例模式,就是
永远保持对象的一个实例。
增强的模块模式,这种模式适合返回自定义对象,也就是构造函数。
	function Desk() {};

	var box = function () {

	var age = 100;

	function run() {

	return '运行中...';

	}

	var desk = new Desk(); //可以实例化特定的对象

	desk.go = function () {

	return age + run();

	};

	return desk;

	}();

	alert(box.go());

	

<think>好的,我需要回答用户关于如何正确释放闭包以避免内存泄漏的问题,包括方法和注意事项。首先,我得回顾一下闭包的基本概念和内存泄漏的原因。闭包会保留对其词法环境的引用,导致外部变量无法被垃圾回收,尤其是当闭包长期存在时。 根据提供的引用资料,引用[1]提到创建大量闭包对象未释放会占用过多内存,需要合理控制生命周期。引用[3]说明闭包是函数加上词法环境,所以需要确保闭包实例被及时销毁。引用[4]中Go语言的例子提到变量逃逸到堆的情况,需要注意生命周期。引用[5]中的JavaScript例子展示了通过立即执行函数解决循环中闭包的问题,这可能涉及到正确管理闭包的作用域。 接下来,我需要整理释放闭包的方法。可能的方法包括手动解除引用、控制闭包作用域、避免循环引用、使用弱引用等。注意事项方面,需要关注闭包的生命周期,避免意外捕获大对象,注意事件监听器的移除,以及框架中的内存管理机制。 需要注意不同编程语言的差异,比如JavaScript中的事件监听器需要显式移除,而Go语言有垃圾回收机制但需注意变量逃逸。还要举例说明,比如在JavaScript中如何解除闭包引用,或者在循环中正确使用闭包避免泄漏。 最后,确保回答结构清晰,分点列出方法和注意事项,并引用相关资料作为支持。同时,检查是否有遗漏的重要点,比如框架中的内存管理工具或弱引用的具体应用。</think>### 如何正确释放闭包以避免内存泄漏? #### 释放闭包的方法 1. **手动解除引用** 将闭包赋值为 `null` 或 `undefined`,显式断开对闭包的引用,使其可被垃圾回收。 ```javascript let closure = (function() { let data = "large data"; return function() { console.log(data); }; })(); // 使用后解除引用 closure = null; // 触发垃圾回收[^5] ``` 2. **控制闭包作用域** 通过限制闭包的生命周期,确保其仅在必要的作用域内存在。例如,在函数内部定义的闭包,若未传递到外部,函数执行完毕后会自动释放[^4]。 ```go func main() { count := 0 func() { count++ // 闭包仅在 main 函数内部使用,执行后自动释放 }() } ``` 3. **避免循环引用** 若闭包引用了外部对象(如 DOM 元素),而该对象又引用了闭包,会导致内存泄漏。需手动解除循环引用。 ```javascript function init() { let element = document.getElementById("button"); element.onclick = function() { /* 闭包引用 element */ }; // 解决方法:在不需要时移除事件监听 element.onclick = null; } ``` 4. **使用弱引用(WeakMap/WeakSet)** 在支持弱引用的语言(如 JavaScript)中,使用 `WeakMap` 或 `WeakSet` 存储对象,避免闭包强引用导致对象无法释放。 ```javascript let weakMap = new WeakMap(); let key = { id: 1 }; weakMap.set(key, "data"); // key 是弱引用,不影响垃圾回收 ``` --- #### 注意事项 1. **关注闭包生命周期** - 若闭包被全局变量、长期存活的对象(如事件监听器)引用,其词法环境中的变量会一直存在[^3]。 - 示例:未移除的事件监听器会导致闭包无法释放[^2]。 2. **避免意外捕获大对象** 闭包会保留其词法作用域内的所有变量,即使仅使用其中一部分: ```javascript function createClosure() { let largeData = new Array(1000000).fill(0); return function() { console.log(largeData[0]); }; // 闭包捕获整个 largeData } // 优化:仅保留必要变量 let firstElement = largeData[0]; return function() { console.log(firstElement); }; ``` 3. **框架中的内存管理** - 在 React/Vue 等框架中,组件卸载时需清理闭包相关的副作用(如 `useEffect` 的清理函数)。 ```javascript useEffect(() => { const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); // 组件卸载时释放闭包 }, []); ``` 4. **利用工具检测泄漏** - 使用浏览器开发者工具的 Memory 面板分析内存快照,查找未被释放的闭包[^1]。 - 在 Go 语言中,通过编译参数 `-gcflags="-m"` 检查变量逃逸情况。 --- #### 总结 释放闭包的核心是**切断闭包与外部环境的引用链**,具体可通过手动解除引用、控制作用域、使用弱引用等方法实现。同时需注意闭包生命周期、避免循环引用,并借助工具检测潜在泄漏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值