浅析JavaScript - 闭包

谈谈JS中经常使我摸不着头脑的闭包。

在函数内部定义其他函数,就创建了闭包。

我第一个接触闭包是这样一道题目。(时间过于久远,我记不太清楚了)

1
2
3
4
5
6
7
8
9
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<p></p>
点击相应的li,输出其中对应的值。将值输入在<p>

当然这道题解析一下其实就是一个循环绑定~

引例说完了,来谈谈闭包。

函数

先来谈谈函数这个一等公民。

定义函数有两种方式:一种是 函数声明 ,即function test() { },另一种就是 函数表达式 ,最常见的就是下面的这种匿名函数的方式。

匿名函数即类似var test = function() { },因为function后面没有标识符,这条语句的意思其实是创建一个函数并将它赋值给test

函数声明和函数表达式定义的两种函数有什么区别呢?

1
2
3
4
sayHi()
function sayHi() {
console.log('hello')
}
1
2
3
4
sayHi()
var sayHi = function() {
console.log('hello')
}

上述这两个例子的输出分别是什么?

第一个是输出:hello,而第二个则是会报错sayHi is not a function

函数表达式和其他表达式一样,在使用前应当先赋值。而函数声明,最重要的特征就是 函数声明提升,意思是在执行代码之前会先读取函数声明!

匿名函数的用途

匿名函数有什么用呢?也是一个非常简单的斐波那契数列的例子。

1
2
3
4
5
6
7
function factorial(num) {
if (num <= 1) {
return 1
} else {
return num * factorial(num - 1)
}
}

这样是不安全的,如果外层的factorial( )被修改了名字,指向了另外其他的函数地址,那么上述这段代码就会出错。使用arguments.callee可以解决这个问题。(arguments.callee是一个指向正在执行的函数的指针。

1
2
3
4
5
6
7
function factorial(num) {
if (num <= 1) {
return 1
} else {
return num * arguments.callee(num - 1)
}
}

我们也可以使用 闭包 来解决这个问题。

1
2
3
4
5
6
7
var factorial = (function f(num) {
if (num <= 1) {
return 1
} else {
return num * f(num - 1)
}
})

那么闭包有什么用呢?

闭包

当在函数内部定义了其他函数时,就创建了闭包。

1. 创建块级作用域

在没有letconst关键字的时候,JS中是没有块级作用域这个概念的,那么如果创建一个块级作用域呢?

1
2
3
(function() {
})()

这样就行了,最后的()的意思是立即执行。

2. 创建用于访问私有变量的公有方法

这种方法一般在设计模式中成为特权方法

可以使用下面这种方法创建一个带有私有变量私有方法特权方法的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Test() {
// 私有变量
var privateVariable = 10
// 私有方法
function privateFunction() {
return false
}
// 特权方法
this.publicMethod = function() {
privateVariable ++
return privateFunction()
}
}

但是这种方式的问题是每次创建一个新的实例的时候都会重新创建 特权方法 。

于是可以利用 闭包,创建 静态私有变量解决这个问题

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
(function() {
var privateVariable = 10
function privateFunction() {
return false
}
// 构造函数
MyObject = function() {
}
MyObject.prototype.publicMethod = function() {
privateVariable ++
return privateFunction()
}
})()
```
但是静态私有变量,多个实例间会相互影响。
前面的模式适用于为自定义类型创建私有变量和特权方法的。而模块模式则是为单例创建私有变量和特权方法。
```js
var singleton = function() {
var privateVariable = 10
function privateFunction() {
return false
}
return {
publicProperty: true,
publicMethod: function() {
privateVariable++
return privateFunction()
}
}
}()

这种模式一般用于在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。

重新回到最初的那道题目中。

1
2
3
4
5
6
7
8
9
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<p></p>
点击相应的li,输出其中对应的值。将值输入在<p>

解析一下就是循环绑定事件,由于click这个是等待用户点击,是一个异步事件,于是按照最普通的想法,i会变成5(如果你不会闭包,第一意识写出来的代码,无论点击哪个li,p内显示的都是5。)

按照块级作用域的想法,我们应该将 i 保存下来,这样才能在之后使用,于是就使用闭包。

当在函数内部定义了其他函数,就创建了闭包。

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
<html>
<head>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
<p></p>
<script>
// 点击相应的li,输出其中对应的值。将值输入在<p>中
function test() {
var button
var displayArea = document.getElementsByTagName("p")[0]
displayArea.innerHTML = "gg"
for (var i = 0; i < 4; i++) {
button = document.getElementsByTagName("li")[i]
button.addEventListener("click",
(function(i) {
return function() {
displayArea.innerHTML = i+1
} // 就是让i可以保存下来。
})(i), false)
}
}
window.onload = test
</script>
</body>

当然如果是ES6的情况下,直接使用let创建一个块级作用域就可以解决这个问题。

1
2
3
4
5
6
7
8
9
function test() {
var button
var displayArea = document.getElementsByTagName("p")[0]
displayArea.innerHTML = "gg"
for (var i = 0; i < 4; i++) {
button = document.getElementsByTagName("li")[i]
button.addEventListener("click", function() {displayArea.innerHTML = i+1}, false)
}
}

下面这种是自执行的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
function test() {
var button
var displayArea = document.getElementsByTagName("p")[0]
displayArea.innerHTML = "gg"
for (var i = 0; i < 4; i++) {
button = document.getElementsByTagName("li")[i]
button.addEventListener("click",
(function(i) {
displayArea.innerHTML = i+1
alert(i+1)
})(i), false)
}
}