深入js——闭包

前言

闭包一直是很多前端开发人员跨不过去的一个坎,我也是一样。每次看各种文章好像弄懂了,但隔段时间好像又模模糊糊了,再看好像又有了新的理解,但感觉总是不能完全理解透彻。
在理解了变量环境、词法环境和作用域链等概念后,我发现理解起来容易多了,这次感觉是真的理解了。
同样,本文的内容是基于对作用域链和变量对象的理解,因此在阅读本文之前,最好先看下深入js——变量对象深入js——作用域链

如何定义闭包

不同平台和书籍对闭包的定义都不一样:

  • MDN:函数与对其状态即词法环境的引用共同构成闭包。
  • 《高级程序设计》:闭包是指有权访问另一个函数作用域中的变量的函数。
  • 维基百科:是引用了自由变量的函数。

这些定义给我一个感受:里面的每个字我都认识,咋凑在一起就不懂了呢?
我的看法是,不要纠结于一个标准的定义,真正理解闭包如何产生、如何使用才是关键。真正理解了之后每个人都可以有自己对闭包的定义,不必拘泥于具体的文字定义。

闭包

这里我主要通过之前提到的浏览器控制台的scope和[[scope]]来理解闭包。
首先来看一个简单的例子

function bar () {
    var a = 1;
    function foo () {
        console.log(a)
    }
    console.dir(foo)
    return foo
}
bar()()

这是闭包最典型的形式,在foo函数里打断点

右侧Scope非常清楚地告诉我们,这儿有个闭包Closure。
这个Closure表示,当bar的执行上下文被销毁后(此时bar已经执行完毕),foo的作用域链为[Local, Closure(bar), Global],也就是说此时foo仍然能访问到bar函数内的变量a。
执行完函数,查看控制台

可以看到,foo.[[scope]](foo的父级作用域链)里有刚刚提到的Closure(bar)。
根据之前文章讲到的[[scope]]和作用域链,并结合以上分析,我们可以这样理解:
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,即内部函数的作用域链上会存在这些变量的结合,即使外部函数执行结束也不会改变内部函数的作用域链,也就是这些变量依然会保存在内存中,我们把这个变量的集合就是闭包。
现在在回头去看上面的各种定义,虽然各不相同,但其实都是表达同一个意思。

Q:很多人理解闭包一定是外部函数中返回的内部函数
从我们上面提到的例子好像也是这样,但其实完全不一定要返回

function bar () {
	var a = 1;
	function foo () {
		console.log(a)
	}
	foo()
}
bar()

直接在bar内执行foo,打开控制台可以看到Scope的状态与返回foo时是一样的,都会有Closure。
这也很好理解,闭包其实就是与作用域链有关的一个东西,而作用域链只是与函数定义的位置有关,至于函数最终赋值给了谁,在哪里调用,怎么调用都不会影响作用域链,当然也就不会影响是否存在闭包。

Q:注释掉foo中的console.log(a),还会有Closure么?

实践下会发现,执行到在foo函数时,Closure不存在了,foo.[[scope]]中也没有了。
这一点其实能帮我们更好的理解闭包的是什么,闭包就是内部函数对外部函数变量的一个引用集合,所以当内部函数没有用到外部函数的变量,那内部函数的作用域链上也就不需要有外部函数了。
所以,有些文章简单概括闭包是函数内部的函数,是不太准确的。严格来说,必须要引用了外部函数的变量,才算形成了闭包。

所有的函数都是闭包

上面都是在从函数内部的函数角度在讨论闭包,主要是大部分情况下,大家默认的也都只是把上述讨论的场景认为是闭包;其实,所有函数都可以成为闭包。
因为,闭包本质上就是有权访问上层作用域的函数,而所有函数其实都能访问全局上下文Global,也就是所有函数的[[scope]]都有Global,从这个意义上来说,所有函数都是闭包

典型题分析

讲了这么多,最后以《高级程序设计》中的例子来结束本文,因为我一次接触闭包就是看的它,当时基本是蒙的。

function createFunctions() {
	var result = new Array();
	for (var i = 0; i < 3; i++) {
		result[i] = function() {
			return i;
		}
	}
	return result;
}
var funcList = createFunctions()
console.log(funcList[0]())
console.log(funcList[1]())
console.log(funcList[2]())

结果不会打印出0,1,2,而是打印出3个3,这个大家基本都知道。
但是如何能输出0,1,2呢?方案大家也基本都知道。通过创建一个匿名函数的方式加一个闭包:

function createFunctions() {
	var result = new Array();
	for (var i = 0; i < 3; i++) {
		result[i] = function(num) {
			return function() {
				return num;
			}
		}(i)
	}
	return result;
}
var funcList = createFunctions()
console.log(funcList[0]())
console.log(funcList[1]())
console.log(funcList[2]())

两个例子中,都存在闭包,但为什么上面的例子没有得到0,1,2,而下面的方案就可以,我们具体来看下。
打开控制台结合Scope分析会更清楚,但为了保持逻辑清晰,以下就不放图了,大家可以自行调试分析。

错误例子

上面的例子中,result数组的每一项的作用域链都是

[Local,Closure(createFunctions),Global]

而每一项的Closure(createFunctions)是共享的,也就是当createFunctions函数执行完,此时createFunctions中的i的值为3了,所以当执行result数组的任一项时,根据作用域链查找就会查到Closure(createFunctions)里的i,此时i为3,所以就都打印出了3。

正确例子

加了一层闭包后,result数组里各项函数的作用域链就变为了

// result[0]
[Local,Closure({num:0}),Global]
// result[1]
[Local,Closure({num:1}),Global]
// result[2]
[Local,Closure({num:2}),Global]

也就是此时各项函数作用域链里的闭包均是单独创建的,是相互独立的三个不同闭包,并且也独立与createFunctions的作用域。所以当createFunctions执行完后,createFunctions里的i同样是3,但已经不会影响到result数组里的各项函数了。这些函数在执行时,都会顺着自己的作用域链找到相应的Closure,并返回其中的变量num的值。

小结

写完有种感觉,闭包真是一个即使理解了也不太容易用文字讲述清楚的东西,有点只可意会不可言传的意思。
另外,在文中,大家会发现我一会说闭包是一个函数,一会说是一个变量集合。其实,这就和文章开头所说的一样,不用刻意定义闭包是什么,准确来说它是一种使用场景,但至于这个场景里哪部分是闭包,没有深究的必要。

posted @ 2020-01-20 17:05  小丸子的城堡  阅读(265)  评论(0编辑  收藏  举报