JavaScript 闭包理解

本文最后更新于 2024年4月9日 下午

第一次接触闭包时,是在使用Python时遇到了一个功能需求,这个功能需要有一列按钮,按钮i在点击时触发回调函数打印出各自的索引。

我首先想到的是这样的实现方式:

1
2
3
4
5
6
7
8
9
def displayIndex(index):
print(index)

def ButtonSet():
.....
for index in range(5):
ButtonList.append(Button)
ButtonList[index].clicked.connect(displayIndex(index))
.....

但结果并不如人意,每次点击的时候打印出的都是“5”。我因为这个问题苦恼了很长时间,后来BBfat给我指出了这种情况应该使用闭包来做。将代码改成了下面的样子:

1
2
3
4
5
6
7
8
9
10
11
def makeFunc(index):
def displayIndex():
print(index)
return displayIndex

def ButtonSet():
.....
for index in range(5):
ButtonList.append(Button)
ButtonList[index].clicked.connect(makeFunc(index))
.....

这下问题完美解决了,但当时他并没有告诉我这样做的原理,我也就稀里糊涂的过去了。


现在接触到了JavaScript,闭包是这个语言中的一个高级特性,所以我这次决定将他弄懂。

访问原本无法访问的作用域

闭包的作用和变量的作用域息息相关,JavaScript中存在着两种作用域,全局作用域和函数作用域。函数内部可以读取全局作用域的变量,但反之则不可以,也就是作用域链的概念,子作用域可以向上回溯寻找父作用域中的变量,但是父作用域无法读取子作用域中的值。

既然这样,那么是否可以借助孙作用域来读取子作用域中的值,无疑是可以的。

1
2
3
4
5
6
7
8
9
10
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}

var result = f1();
result(); // 999

这就是闭包,即能够读取其他函数中变量的函数,由于函数中的值只能由子函数读取,所以闭包也可以理解为定义在函数中的函数,闭包可以记住诞生的环境,通过他可以访问原本无法访问的作用域。

保存运行状态

闭包的另一个作用就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。

1
2
3
4
5
6
7
8
9
10
11
12
function createIncrementor() {
var count = 5;
return function () {
return count++;
};
}

var inc = createIncrementor();

console.log(inc()) // 5
console.log(inc()) // 6
console.log(inc()) // 7

我们知道函数创建时会在内存中创建属于他的作用域链,关联父作用域,初始化属于自己作用域其中的变量等。

函数运行时会存在运行时上下文,上下文中的变量就是通过复制(而不是引用)函数函数作用域链(保存在内存中)而来的。

上下文所用的内存会由系统检测到不再使用时自动回收,下一次运行时再次复制,所以上下文每次运行时的变量值都会和函数创建(声明)时一样。

但上文中闭包被创建时,闭包同样是一个函数,系统需要为他创建作用域,此时父函数的运行时上下文即为他的父作用域,他引用的变量count是通过指针的方式,指向了父函数运行时上下文的某块内存。

我们可以看到只有被闭包引用的变量被包含进来了,未被引用的temp变量并未被引用。

父函数运行结束,系统开始释放他的运行时上下文,但是发现有一块内存视乎还在被使用(引用),所以并没有释放count所在的内存。父函数变量的状态得以保存。

因为这个变量是通过指针引用的方式被指向的,所以下一次inc运行时,用到这个变量还回去内存中的相应位置寻找,但此时count已经被修改为6而不是5。

可能会有疑惑?子函数使用count的方式为什么一定是指针引用方式,而不是复制?

我们可以类比一下!

函数func属于函数作用域,使用了父作用域(全局)中的变量a,如果不是采用引用的方式,如何解释a的值在函数外一样增加了呢?

所以我们可以得出,子作用域引用父作用域的变量的方式是指针引用。

封装私有属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}

return {
name: name,
getAge: getAge,
setAge: setAge
};
}

var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

这个闭包看起来有些不太一样,这是因为这次返回的变量不是函数了,而是一个对象,我觉得他更像是立即执行函数。

立即执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
var module1 = (function () {
 var _count = 0;
 var m1 = function () {
  //...
 };
 var m2 = function () {
  //...
 };
 return {
  m1 : m1,
  m2 : m2
 };
})();

立即执行函数也可以达到封装属性的作用,因为函数执行后返回的对象只有变量的存取方法,而无法直接获取到变量本身。


OK! 让我们回到开头的这个问题,JavaScript也会遇到开头Python的那种问题。

比如:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>JS Bin</title>
<style>
ul {
list-style: none;
padding: 0;
}
li {
border: 1px solid black;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<script>
var ul = document.getElementsByTagName("ul")[0];

var liList = ul.getElementsByTagName("li");
for (var i = 0; i < 6; i++) {
liList[i].onclick = function (i) {
alert(i); // 为什么 alert 出来的总是 6,而不是 1、2、3、4、5
};
}
</script>
</body>
</html>

可以看到和Python中出现的情况类似。

我们分析一下为什么?

1.gif

可以看到,回调函数function中的i是引用方式进行传递的,在执行之前,他会随着全局环境(父作用域)中的值改变,在循环结束时,父作用域中的i的值已经变成了6,因此所有引用的值也变成了6,这个时候点击回调函数调用,弹出的值则为6。

我们要做的就是,在绑定的同时就将以后执行时的参数确定。

利用立即执行函数则可解决这个问题:

1
2
3
4
5
6
7
8
9
10
var ul = document.getElementsByTagName("ul")[0];

var liList = ul.getElementsByTagName("li");
for (var i = 0; i < 6; i++) {
!(function (i) {
liList[i].onclick = function (i) {
alert(i); // 为什么 alert 出来的总是 6,而不是 1、2、3、4、5
};
})(i);
}

利用立即执行函数与闭包结合也可以解决,因为闭包可以保存运行时状态,立即执行函数执行则让他保存下执行时的状态,而执行“立即执行函数”时就是在进行绑定,所以可以解决这个问题:

1
2
3
4
5
6
7
8
9
10
var ul = document.getElementsByTagName("ul")[0];

var liList = ul.getElementsByTagName("li");
for (var i = 0; i < 6; i++) {
liList[i].onclick = (function (i) {
return function () {
alert(i);
};
})(i);
}

晚上看了看ES6的特性,发现使用新的特性一句话就可以解决这个问题!!!

1
2
3
4
5
6
7
8
var ul = document.getElementsByTagName("ul")[0];

var liList = ul.getElementsByTagName("li");
for (let i = 0; i < 6; i++) {
liList[i].onclick = function () {
alert(i);
};
}

3


JavaScript 闭包理解
https://siegelion.cn/2021/01/29/JavaScript闭包理解/
作者
siegelion
发布于
2021年1月29日
许可协议