理解闭包
Chao 工程师

前言

要理解闭包,我们需要了解js上下文环境和作用域链的知识,有兴趣的同学可以看看下面这两篇,或许对你有些许帮助:

javascript中神奇的预编译

javascript的作用域与作用域链

概念

闭包就是能够读取其他函数内部变量的函数。

首先我们来了解什么是闭包,总结为一句话就是:函数作为返回值、函数作为参数传递

第一种:函数为返回值

1
2
3
4
5
6
7
8
9
10
11
function test1() {
function test2(){
var b = 2
console.log(a)
}
var a = 1
return test2
}
var c = 3
var test3 = test1()
test3()

第二种:函数为参数

1
2
3
4
5
6
7
8
9
10
var max = 10
var fn = function(x) {
if (x > max) {
console.log(x)
}
}
(function(f){
var max = 100
f(15)
})(fn)

原理

我们来使用上面第一个例子来解读

1、当test1函数被定义时,系统生成[[scope]]属性,[[scope]]保存该函数当作用域链,该作用域链的第0位存储当前环境下的全局执行上下文GO,GO里存储的全局下的所有对象,其中包含函数test1和全局变量c
image

2、当test1被执行时,生成test1函数的AO储存在作用域链的顶端,同时GO被排在第1位,而此时test2被定义,此时test2的作用域链和test1相同
image

3、当函数test1执行结束时,因为test2被返回到外部,且被全局变量test3接收。这时test1的AO并没有被销毁,只是把线剪断了,因为test2没有执行,只是被返回到外部,并把test1(也是test2)的AO携带出去了,test2的作用域链还连着的。
image

4、test3执行,即test2开始执行,此时test2拥有自己的作用域链,并且在该作用域链的顶端增加了test2的AO。根据作用域链原理,当执行到console.log(a)时,现在test2的AO里寻找a,此时没有找到,让后去test1的AO里查找,找到了则此时操作的AO为test1的AO。当再次执行test3时,实际操作的仍然是原来test1的AO。
image

5、当test3执行结束后,test2的AO被销毁,但是原来test1的AO被test2携带到了外部导致无法被销毁,test2的AO依然存在。所以,我们知道为什么说闭包会导致内存泄漏了吧!
image

注意: 当内部函数被返回到外部并保存时,一定会产生闭包,闭包会产生原来的作用域链不释放,过度的闭包可能会导致内存泄漏,或加载过慢。

夯实基础

例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function test1() {
var n = 100
function add() {
n++
console.log(n)
}
function reduce () {
n--
console.log(n)
}
return [add, reduce]
}
var arr = test1()
arr[0]() // 99
arr[1]() // 100
arr[0]() // 101
arr[0]() // 102

例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function breadMgr(num) {
var breadNum = num || 10
function supply() {
breadNum += 10
console.log(breadNum)
}
function sale(){
breadNum--
console.log(breadNum)
}
return [supply, sale]
}
var breadMgr = breadMgr(50)
breadMgr[0]()
breadMgr[1]()
breadMgr[1]()
breadMgr[1]()

例三:经典面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
function test() {
var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i + ' ')
}
}
return arr;
}
var myArr = test();
myArr[0]() // 10
myArr[1]() // 10
myArr[1]() // 10

实例解读:
1、for循环中添加了10个方法,被储存在数组arr中。
2、这些方法并未被执行,而是被返回到了外部并赋值给了变量myArr,因为未被执行,所以此时匿名函数的AO为函数test()的AO。
3、当执行myArr中当方法时,此时匿名函数生成自己的AO。
4、当程序执行到console.log(i)时,js会自动先去匿名函数的AO中寻找变量i,此时没有找到,然后去test函数中寻找,找到了,而此时i的值在test函数执行完之后变成了10,所以,不论怎么执行myArr的方法,i的值始终为10。

解决办法:
方法一:通过外部力量强行改变

1
2
3
4
5
6
7
8
9
10
11
function test() {
var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = function (j) {
console.log(j + ' ')
}
}
return arr;
}
var myArr = test();
myArr[1](j)

方法二:函数自执行, 通过立即执行函数使i随for循环时改变

1
2
3
4
5
6
7
8
9
10
11
12
13
function test() {
var arr = [];
for (var i = 0; i < 10; i++) {
(function(j){
arr[i] = function (j) {
console.log(j + ' ')
}
}(i))
}
return arr;
}
var myArr = test();
myArr[1]()
 Comments