剖析闭包的生成

269 阅读3分钟

首先什么是闭包?

《JavaScript高级程序设计》这样描述闭包:

闭包是指有权访问另一个函数作用域中的变量的函数。

《你不知道的JavaScript》这样描述:

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

讲解闭包

上面已经说到,当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 分析下面一段代码:

function fn1() {
    var name = 'Apple';
	function fn2() {
		console.log(name);
	}
    fn2();
}
fn1();   // Apple

根据《JavaScript高级程序设计》和《JavaScript权威指南》来说,上面的代码已经产生闭包了。fn2访问到了fn1的变量,满足了条件“有权访问另一个函数作用域中的变量的函数”,fn2本身是个函数,所以满足了条件“所有的JavaScript函数都是闭包”。

这其实并不是闭包,fn2执行时实在fn1内部通过词法作用域找到name的值并打印。 再看一段代码:

function fn1() {
	var name = 'iceman';
	function fn2() {
		console.log(name);
	}
	return fn2;
}
var fn3 = fn1();
fn3();

这样就清晰地展示了闭包:

  • fn2的词法作用域能访问fn1的作用域

  • 将fn2当做一个值返回

  • fn1执行后,将fn2的引用赋值给fn3

  • 执行fn3,输出了变量name

我们知道通过引用的关系,fn3就是fn2函数本身。执行fn3能正常输出name,这不就是fn2能记住并访问它所在的词法作用域,而且fn2函数的运行还是在当前词法作用域之外了。

正常来说,当fn1函数执行完毕之后,其作用域是会被销毁的,然后垃圾回收器会释放那段内存空间。而闭包却很神奇的将fn1的作用域存活了下来,fn2依然持有该作用域的引用,这个引用就是闭包。

总结:某个函数在定义时的词法作用域之外的地方被调用时产生闭包,闭包可以使该函数访问定义时的词法作用域。

注意:对函数值的传递可以通过其他的方式,并不一定值有返回该函数这一条路,比如可以用回调函数:

function fn1() {
	var name = 'iceman';
	function fn2() {
		console.log(name);
	}
	fn3(fn2);
}
function fn3(fn) {
	fn();
}
fn1();

本例中,将内部函数fn2传递给fn3,当它在fn3中被运行时,它是可以访问到name变量的。

所以无论通过哪种方式将内部的函数传递到所在的词法作用域以外,它都持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。

再次解释闭包

观察以下代码:

for (var i = 1; i <= 10; i++) {
	setTimeout(function () {
		console.log(i);
	}, 1000);
}

在这段代码中,我们对其的预期是输出1~10,但却输出10次11。这是因为setTimeout中的匿名函数执行的时候,for循环都已经结束了,for循环结束的条件是i大于10,所以当然是输出10次11咯。

究其原因:i是声明在全局作用中的,定时器中的匿名函数也是执行在全局作用域中,那当然是每次都输出11了。

原因知道了,解决起来就简单了,我们可以让i在每次迭代的时候,都产生一个私有的作用域,在这个私有的作用域中保存当前i的值。

for (var i = 1; i <= 10; i++) {
	(function () {
		var j = i;
		setTimeout(function () {
			console.log(j);
		}, 1000);
	})();
}

闭包的应用

闭包的应用比较典型是定义模块,我们将操作函数暴露给外部,而细节隐藏在模块内部:

function module() {
	var arr = [];
	function add(val) {
		if (typeof val == 'number') {
			arr.push(val);
		}
	}
	function get(index) {
		if (index < arr.length) {
			return arr[index]
		} else {
			return null;
		}
	}
	return {
		add: add,
		get: get
	}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));

参考链接