关于闭包也许你不知道的事

closure

当一个函数A返回一个内联函数B,即使函数A执行完,函数B也能访问函数A作用域内的变量。这就是一个闭包——————本质上闭包是将函数内部和外部连接起来的一座桥梁。

1
2
3
4
5
6
7
8
9
10
function foo(message) {
function closure() {
console.log(message)
};
return closure;
}
// 使用
var bar = foo("hello closure!");
bar()// 返回 'hello closure!'

在函数foo内创建的函数closure对象是不能被回收掉的,因为它被全局变量bar引用,处于一直可访问状态。通过执行bar()可以打印出hello closure!。如果想释放掉可以将bar = null即可。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Closure</title>
</head>
<body>
<p>不断单击【click】按钮</p>
<button id="click_button">Click</button>
<script>
function f() {
var str = Array(10000).join('#');
var foo = {
name: 'foo'
}
function unused() {
var message = 'it is only a test message';
str = 'unused: ' + str;
}
function getData() {
return 'data';
}
return getData;
}
var list = [];
document.querySelector('#click_button').addEventListener('click', function () {
list.push(f());
}, false);
</script>
</body>
</html>

这里结合Chrome的Devtools->Memory工具进行分析,操作步骤如下:

注:最好在隐身窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 选中【Record allocation timeline】选项
  2. 执行一次CG
  3. 单击【start】按钮开始记录堆分析
  4. 连续单击【click】按钮十多次
  5. 停止记录堆分析

closure

上图中蓝色柱形条表示随着时间新分配的内存。选中其中某条蓝色柱形条,过滤出对应新分配的对象:

closure

查看对象的详细信息:

closure

从图可知,在返回的闭包作用链(Scopes)中携带有它所在函数的作用域,作用域中还包含一个str字段。而str字段并没有在返回getData()中使用过。为什么会存在在作用域中,按理应该被GC回收掉的?

原因是在相同作用域内创建的多个内部函数对象是共享同一个变量对象(variable object)。如果创建的内部函数没有被其他对象引用,不管内部函数是否引用外部函数的变量和函数,在外部函数执行完,对应变量对象便会被销毁。反之,如果内部函数中存在有对外部函数变量或函数的访问(可以不是被引用的内部函数),并且存在某个或多个内部函数被其他对象引用,那么就会形成闭包,外部函数的变量对象就会存在于闭包函数的作用域链中。这样确保了闭包函数有权访问外部函数的所有变量和函数。了解了问题产生的原因,便可以对症下药了。对代码做如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function f() {
var str = Array(10000).join('#');
var foo = {
name: 'foo'
}
function unused() {
var message = 'it is only a test message';
// str = 'unused: ' + str; //删除该条语句
}
function getData() {
return 'data';
}
return getData;
}
var list = [];
document.querySelector('#click_button').addEventListener('click', function () {
list.push(f());
}, false);

getData()和unused()内部函数共享f函数对应的变量对象,因为unused()内部函数访问了f作用域内str变量,所以str字段存在于f变量对象中。加上getData()内部函数被返回,被其他对象引用,形成了闭包,因此对应的f变量对象存在于闭包函数的作用域链中。这里只要将函数unused中str = 'unused: ' + str;语句删除便可解决问题。

closure

查看一下闭包信息:

closure

总结

闭包是JavaScript中很重要的概念,理解好它对我们掌握JavaScript很关键。也许你个人认为你对它很了解,但当因它产生问题时,才感悟自己对它了解甚少。如本文提到:在相同作用域内创建的多个内部函数对象是共享同一个变量对象。不管怎么样希望你能从本文中有所收获~。~

参考文章