9分钟,搞明白闭包

8,752 阅读5分钟

闭包——JS三座大山之一,对很多前端开发者来说是个非常头疼的知识点。本文将花费大约9分钟时间,用4组非常简单的例子来对比理解闭包,也许会改变大家之前对闭包的理解。

在案例对比之前,先给大家一个观点:闭包只是一种现象。在此,我们不要试图用官方的定义,以扣字眼的方式去理解闭包的含义,那样会进入一个理解的误区。我们的目的是找出函数执行时出现闭包现象的条件。所以我们来用4组(共9个demo,每分钟可以看完一个)案例剖析下,如果看完能搞清楚闭包这种现象是如何产生的,那就大功告成了。

第一组、用全局变量引用再执行

var m, n;//全局变量

function outer_a1() {
	var num = 10;
	function inner() {
		console.log(num) //打印了outer_a1中定义的num
		debugger
	}
	m = inner;
}
outer_a1();
m();

function outer_a2() {
	var num = 10;
	function inner() {
		console.log("just console") //只做了简单的打印
		debugger
	}
	n = inner;
}
outer_a2();
n();

上面两张图片是分别执行outer_a1()和outer_a2()后再执行m()和n()时,到其中的debugger断点后,chrome控制台的截图,我们可以看出第一个出现了Closure并包含了变量num:10,第二张图并没有出现Closure。先不做解释,继续第二组案例对比。

第二组、内部直接执行

function outer_b1() {
	var num = 15;
	function inner() {
		console.log(num) //打印了outer_b1中定义的num
		debugger
	}
	inner()
}
outer_b1();

function outer_b2() {
	var num = 15;
	function inner() {
		console.log("just console") //只做了简单的打印
		debugger
	}
	inner()
}
outer_b2();

第二组跟第一组差不多,只是改为了在outer_b1和outer_b2中直接执行了inner,我们可以发现还是第一个产生了Closure包含变量num:15(备注:如果把var num = 15;放在inner()执行的后面,根据变量提升规则,Closure中的变量num值就是undefined),而第二个没有产生Closure,继续第三组:

第三组、return后再执行

function outer_c1(s) {
	var num = 20;
	function inner() {
		console.log(s + num) //打印包含了outer_c1形参s和定义的num
		debugger
	}
	return inner
}
outer_c1()();

function outer_c2(s) {
	var num = 20;
	function inner() {
		console.log("just console") //只做了简单的打印
		debugger
	}
	return inner
}
outer_c2()();

第三组使用了return把内部的inner返回出来,然后分别执行outer_c1()()和outer_c2()()来运行内部的inner,发现还是第一个产生了Closure而第二个没有Closure,不同的是第一个又引用了outer_c1的形参s,所以Closure中的变量也多了一个s,不过执行outer_c1()时并没有传入实参,所以s的值是undefined。

这三组案例实际都很相似,每组两个demo之间的唯一区别就是:前一个demo打印了属于outer函数的私有局部变量,而后一个demo只是做了简单的打印。到此大致可以确定,闭包的产生跟执行方式并没有特别的关联,而是看内部函是否引用外部函数的私有变量来确定的。然后再来看下最后一组:

第四组、多种方式一起

var x, y, z; //全局变量

function outer_d1(s) {
	var num = 25;
	function inner1() {
		console.log(s) //打印形参s
		debugger
	}
	y = inner1;
	function inner2() {
		console.log("just console") //只做了简单的打印
		debugger
	}
	inner2();
	function inner3() {
		console.log(num) //打印定义的num
		debugger
	}
	return inner3
}
outer_d1("i am x");

function outer_d2(s) {
	var num = 25;
	function inner1() {
		console.log(s) //打印形参s
		debugger
	}
	
	function inner2() {
		console.log("just console") //只做了简单的打印
		debugger
	}

	function inner3() {
		console.log(num) //打印定义的num
		debugger
	}
	
	inner2();
}
outer_d2("i am y");

function outer_d3(s) {
	var num = 30;
	function inner1() {
		console.log("just console") //只做了简单的打印
		debugger
	}
	z = inner1;
	function inner2() {
		console.log("just console") //只做了简单的打印
		debugger
	}
	inner2();
	function inner3() {
		console.log("just console") //只做了简单的打印
		debugger
	}
	return inner3
}
outer_d3("i am z")();
z();

本组对比了三个例子,第一组和第二组稍有不同,第一个内部所有的inner最终都执行了,而第二组只执行了一个inner2,。运行结果就不贴图了,大家可以自己运行下,结果会发现如下现象:

  • 1、运行到outer_d1内部的各个inner时都会有Closure并且Closure的变量都是num和s;
  • 2、运行到outer_d2内部的inner2时也会有Closure,并且Closure的变量也都是num和s;
  • 3、运行到outer_d3内部的各个inner时没有Closure。

outer_d3没有产生闭包很好理解,因为内部函数都是简单打印,但是为什么outer_d1和outer_d2内部的inner2也都是简单打印,但是执行的时候也产生了闭包呢?

这是因为他们内部的inner1和inner3都引用了outer的私有变量,此时outer_d1和outer_d2就已经成为了一个闭包了,所以即使inner2什么都不打印,也已经在这个闭包环境中了,执行时也会看到产生的Clouser,形成的Clouser中的变量是outer被所有inner函数引用的局部私有变量。

总结

根据上面的四组对比案例,我们可以总结出闭包是函数在特定情况下执行产生的一种现象,这种现象产生的条件为:当一个函数(outer)运行时它的形参或者它的私有变量(num,s)被内部函数(inner)所引用。满足这个条件时,那么这个outer就会形成闭包,这个闭包所包含的变量为outer本身运行时被内部函数引用到的所有形参和局部变量。跟内部函数被return或者不return,被执行或者不执行没有关系。