js中的闭包

谈谈闭包

最开始我以为闭包就是一个函数里内嵌了一个内部函数,内部函数可以访问外部函数的私有变量,并使得其他函数可以访问到该私有变量,其实这只是一小部分,真正的闭包作用不止如此。

一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数,因为到ES6才有块级作用域的概念)。它有权访问另一个函数作用域中的变量的函数。

故主要作用归结为两个:

  1. 可以在函数外部访问到函数内部的函数的局部变量。
  2. 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。

深层原因

  1. 我们都知道在js执行时一个函数时都会创建一个作用域函数,将函数中的局部变量(函数的形参也是局部变量(保存进去,伴随那些传入的函数变量一起被初始化。当函数执行完毕时,由于返回的函数使用了某个私有属性,,所以返回的函数中保存了一个指向该私有属性的引用,故不会被回收。
1
2
3
4
5
6
7
8
9
10
11
function A(age) {
var name = 'wind';
var sayHello = function() {
console.log('hello, '+name+', you are '+age+' years old!');
};
return sayHello;
}
var wind = A(20);
wind(); // hello, wind, you are 20 years old!

其作用域函数对象为{ age:20, name:'wind'};
  1. 每次调用一次外部函数就产生一个新的闭包,新的作用域,以前的闭包依旧存在互不影响。
  2. 同一个闭包会保留上一次的状态,当它被再次调用时会在上一次的基础上进行。
1
2
3
4
5
6
7
8
9
function add(){
var nnum = 42;
return function(){ console.log(num++); }
}
var a = A();
a(); //42
a(); //43
var b = A(); //重新调用A(),形成新闭包,有了新的作用域对象
b(); //42

4、在外部函数中存在的多个函数 “ 同生共死 ”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var fun1, fun2, fun3;
function A() {
var num = 42;
fun1 = function() { console.log(num); }
fun2 = function() { num++; }
fun3 = function() { num--; }
}

A();
fun1(); // 42
fun2();
fun2();
fun1(); // 44
fun3();
fun1(); //43

var old = fun1;

A();
fun1(); // 42
old(); // 43 上一个闭包的fun1()

三个函数被同时声明并且都可以对作用域对象的属性(局部变量)进行访问与操作。

由于函数不能有多个返回值,所以我用了全局变量。我们再次可以看出在我们第二次调用A()时产生了一个新的闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function buildArr(arr){
var result = [];
for(var i = 0; i < arr.length; i++){
var item = 'item' + i;
result.push(function(){
console.log(item + ' ' + arr[i])
});
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnList[0](); //item2 undefined
fnList[1](); //item2 undefined
fnList[2](); //item2 undefined

怎么会这样呢?我们预想的三个输出应该是 item0 1, item1 2, item2 3。为什么结果却是返回的result数组里面存储了三个 item2 undefined ?

我们上文中提到过两点:

  1. 闭包在返回的时候对作用域对象有一个引用。
  2. 在外部函数中存在的内部函数的“同生共死”。

我们的for循环为外部函数创建了多个“同生共死”的内部函数,他们都共享一个环境,而当result数组返回时,所有内部函数都引用了同一个作用域对象:

1
2
3
4
5
var bArr = {
item: 'item2',
i : 3,
arr: [1,2,3]
}

为什么作用域对象是这样?就拿我们上面的例子来说,当循环全部结束时,作用域对象中的属性i正好是i++之后的3,而arr[3]是没有值的,所以为undefined。

注意:在最后一次循环的时候即i = 2时,当i++,i = 3的循环条件不满足循环结束,此时的item的值已经被保存下来了,所以此时的arr[i]为arr[3],而item为item2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function buildArr(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
result.push( (function(n) {
return function() {
var item = 'item' + n;
console.log(item + ' ' + arr[n]);
}
})(i));
}
return result;
}

var fnlist = buildArr([1,2,3]);
fnlist[0](); // item0 1
fnlist[1](); // item1 2

我们可以用一个自执行函数将i绑定,这样i的每一个状态都会被存储,答案就和我们预期的一样了。

所以以后在使用闭包的时候遇到循环变量我们要习惯性的想到用自执行函数来绑定它。

1
2
3
4
5
6
7
8
9
10
function buildArr(arr){
var result = [];
for(let i = 0; i <arr.length; i++){
let item = 'item' + i;
result.push(function(){console.log(item + '' + arr[i])});
}
return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0](); //item0 1

使用let代替,主要是利用let会创建块级作用域的特性。

闭包的用途
  1. 读取函数内部的变量。
  2. 让这些变量的值始终保持在内存中,不会在函数被调用后被自动清除。
  3. 方便调用上下文的局部变量,利于代码的封装。
闭包应用场景
  1. setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param){
alert(param)
},1000)


//通过闭包可以实现传参效果
function func(param){
return function(){
alert(param)
}
}
var f1 = func(1);
setTimeout(f1,1000);
  1. 作为回调函数
1
2
3
4
5
function changeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
  1. 封装变量
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>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>闭包模拟私有方法</title>
<link rel="stylesheet" href="">
</head>
<body>
<script>
//用闭包定义能访问私有函数和私有变量的公有函数。
var counter = (function(){
var privateCounter = 0; //私有变量
function change(val){
privateCounter += val;
}
return {
increment:function(){ //三个闭包共享一个词法环境
change(1);
},
decrement:function(){
change(-1);
},
value:function(){
return privateCounter;
}
};
})();

console.log(counter.value());//0
counter.increment();
counter.increment();//2
//共享的环境创建在一个匿名函数体内,立即执行。
//环境中有一个局部变量一个局部函数,通过匿名函数返回的对象的三个公共函数访问。

</script>
</body>
</html>
  1. 为节点循环绑定click事件
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="">
</head>
<body>

<p id="info">123</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>

<script>
function showContent(content){
document.getElementById('info').innerHTML = content;
};

function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
var item = infoArr[i];
document.getElementById(item.id).onfocus = function(){
showContent(item.content)
}
}
}
setContent()
//循环中创建了三个闭包,他们使用了相同的词法环境item,item.content是变化的变量
//当onfocus执行时,item.content才确定,此时循环已经结束,三个闭包共享的item已经指向数组最后一项。



/**
* 解决方法1 通过函数工厂,则函数为每一个回调都创建一个新的词法环境
*/
function showContent(content){
document.getElementById('info').innerHTML = content;
};

function callBack(content){
return function(){
showContent(content)
}
};

function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
var item = infoArr[i];
document.getElementById(item.id).onfocus = callBack(item.content)
}
}
setContent()

/**
* 解决方法2 绑定事件放在立即执行函数中
*/
function showContent(content){
document.getElementById('info').innerHTML = content;
};

function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
(function(){
var item = infoArr[i];
document.getElementById(item.id).onfocus = function(){
showContent(item.content)
}
})()//放立即执行函数,立即绑定,用每次的值绑定到事件上,而不是循环结束的值
}
}
setContent()

/**
* 解决方案3 用ES6声明,避免声明提前,作用域只在当前块内
*/
function showContent(content){
document.getElementById('info').innerHTML = content;
};

function setContent(){
var infoArr = [
{'id':'email','content':'your email address'},
{'id':'name','content':'your name'},
{'id':'age','content':'your age'}
];
for (var i = 0; i < infoArr.length; i++) {
let item = infoArr[i]; //限制作用域只在当前块内
document.getElementById(item.id).onfocus = function(){
showContent(item.content)
}
}
}
setContent()
</script>
</body>
</html>

推荐博文:https://blog.csdn.net/qq_36276528/article/details/70049825

文章目录
  1. 1. 谈谈闭包
    1. 1.0.1. 深层原因
      1. 1.0.1.0.1. 闭包的用途
      2. 1.0.1.0.2. 闭包应用场景
|