闭包的几种概念理解:

  1. 能够访问另一个函数作用域的变量的函数。清晰的讲:闭包就是一个函数,这个函数能够访问其他函数的作用域中的变量
  2. 闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。直观的说就是形成一个不销毁的栈环境。
  3. 闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。
  4. 闭包是指有权访问另一个函数作用域中的变量的函数
  5. 能够读取其他函数内部变量的函数。或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用。

我们都知道,js的作用域分两种,全局和局部,基于我们所熟悉的作用域链相关知识,我们知道在js作用域环境中访问变量的权利是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量,反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数。


我们首先知道闭包有3个特性:

①函数嵌套函数

②函数内部可以引用函数外部的参数和变量

③参数和变量不会被垃圾回收机制回收


闭包的理解

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function init() {
var name = "Chrome"; //创建局部变量name和局部函数alertName

function alertName() { //alertName()是函数内部方法,是一个闭包
alert(name); //使用了外部函数声明的变量,内部函数可以访问外部函数的变量
}
alertName();
}
init();
//一个变量在源码中声明的位置作为它的作用域,同时嵌套的函数可以访问到其外层作用域中声明的变量

function outFun(){
var name = "Chrome";
function alertName(){
alert(name);
}
return alertName; //alertName被外部函数作为返回值返回了,返回的是一个闭包
}

var myFun = outFun();
myFun();

闭包有函数+它的词法环境;词法环境指函数创建时可访问的所有变量。
myFun引用了一个闭包,闭包由alertName()和闭包创建时存在的“Chrome”字符串组成。
alertName()持有了name的引用,
myFunc持有了alertName()的的访问,
因此myFunc调用时,name还是处于可以访问的状态。


例子2

1
2
3
4
5
6
7
8
9
10
   function a(){
var n = 0;
function inc() {
n++;
console.log(n);
}
inc();
inc();
}
a();

例子3

1
2
3
4
5
6
7
8
9
10
11
function a(){
var n = 0;
function inc(){
n++;
console.log(n);
}
return inc;
}
var c = a();
c(); //控制台输出1
c(); //控制台输出2

看看是怎么执行的:

var c = a(),这一句 a()返回的是函数 inc,那这句等同于 var c = inc;
c(),这一句等同于 inc(); 注意,函数名只是一个标识(指向函数的指针),而()才是执行函数。
后面三句翻译过来就是: var c = inc; inc(); inc();,跟第一段代码有区别吗? 没有。

为啥要这样写?
我们知道,js的每个函数都是一个个小黑屋,它可以获取外界信息,但是外界却无法直接看到里面的内容。将变量 n 放进小黑屋里,除了 inc 函数之外,没有其他办法能接触到变量 n,而且在函数 a 外定义同名的变量 n 也是互不影响的,这就是所谓的增强“封装性”。

而之所以要用 return 返回函数标识 inc,是因为在 a 函数外部无法直接调用 inc 函数,所以 return inc 与外部联系起来。


例子4

1
2
3
4
5
6
7
8
9
10
11
function add(x){
return function(y){
return x + y;
};
}

var addFun1 = add(4);
var addFun2 = add(9);

console.log(addFun1(2)); //6
console.log(addFun2(2)); //11

add接受一个参数x,返回一个函数,它的参数是y,返回x+y。
add是一个函数工厂,传入一个参数,就可以创建一个参数和其他参数求值的函数。
addFun1和addFun2都是闭包。他们使用相同的函数定义,但词法环境不同,addFun1中x是4,后者是5。


例子5

1
2
3
4
5
6
7
8
9
10
11
function fn() {
var num = 3;
return function() {
var n = 0;
console.log(++n);
console.log(++num);
}
}
var fn1 = fn();
fn1(); // 1 4
fn1(); // 1 5

一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,这时候相当于fn1=function(){var n = 0 … },并且匿名函数内部引用着fn里的变量num,所以变量num无法被销毁,而变量n是每次被调用时新创建的,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后就剩下孤零零的num,于是这里就产生了内存消耗的问题。


例子6
经典例子-定时器与闭包
写一个for循环,让它按顺序打印出当前循环次数

1
2
3
4
5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, 100);
}

按照预期它应该依次输出1 2 3 4 5,而结果它输出了五次5,这是为什么呢?原来由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经编程5,所以打印出来五个5,那么我们为了实现预期结果应该怎么改这段代码呢?

解决办法:
1、(ps:如果把for循环里面的var变成let,也能实现预期结果 ES6新增 利用块级作用域(最简单,推荐))

1
2
3
4
5
6
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, 100);
}
// 控制台输出 0,1,2,3,4

2、利用自执行函数,将i作为参数传入

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i)
}, 100);
})(i);
}
// 控制台输出 0,1,2,3,4

引入闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递,100毫秒后同时打印出1 2 3 4 5。

那如果我们想实现每隔100毫秒分别依次输出数字,又该怎么改呢?

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i)
}, i * 100);
})(i);
}
// 控制台输出 0,1,2,3,4

在这段代码中,相当于同时启动3个定时器,i*100是为4个定时器分别设置了不同的时间,同时启动,但是执行时间不同,每个定时器间隔都是100毫秒,实现了每隔100毫秒就执行一次打印的效果。

2、利用setTimeout的第三个参数,将i作为参数传入function

1
2
3
4
5
6
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, 100, i);
}
// 控制台输出 0,1,2,3,4

例子7
闭包作为函数传递

1
2
3
4
5
6
7
8
9
10
11
12
var num = 15;
var fn1 = function(x) {
if (x > num) {
console.log(x);
}
}

void function(fn2) {
var num = 100;
fn2(30);
}(fn1);
//控制台打印 30

在这段代码中,函数fn1作为参数传入立即执行函数中,在执行到fn2(30)的时候,30作为参数传入fn1中,这时候if(x>num)中的num取的并不是立即执行函数中的num,而是取创建函数的作用域中的num这里函数创建的作用域是全局作用域下,所以num取的是全局作用域中的值15,即30>15,打印30


闭包应用场景

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="">
</head>
<style>
body {
font-size: 12px;
}

h1 {
font-size: 1.5rem;
}

h2 {
font-size: 1.2rem;
}
</style>

<body>

<p>欧力给</p>
<h1>嘤嘤嘤</h1>
<h2>啊啊啊</h2>

<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

<script>
function changeSize(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}

var size12 = changeSize(12);
var size14 = changeSize(14);
var size16 = changeSize(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
//我们定义行为,然后把它关联到某个用户事件上(点击或者按键)。我们的代码通常会作为一个回调(事件触发时调用的函数)绑定到事件上
</script>
</body>

</html>

2、闭包应用场景之封装变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//用闭包定义能访问私有函数和私有变量的公有函数。
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();
console.log(counter.value()); //1
counter.increment();
console.log(counter.value()); //2

共享的环境创建在一个匿名函数体内,立即执行。
环境中有一个局部变量一个局部函数,通过匿名函数返回的对象的三个公共函数访问。

3、闭包应用场景之为节点循环绑定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
<!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已经指向数组最后一项。

</script>
</body>
</html>

解决方法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
/**
* 解决方法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     绑定事件放在立即执行函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 解决方法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声明,避免声明提前,作用域只在当前块内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 解决方案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()

解决方案4     使用 forEach()来遍历helpText数组并给每一个

添加一个监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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'
}];
infoArr.forEach(function(text) {
document.getElementById(text.id).onfocus = function() {
showContent(text.content);
}
});
}
setContent()


补充

从堆栈的角度看待js函数

基本变量的值一般都是存在栈内存中,而对象类型的变量的值存储在堆内存中,栈内存存储对应空间地址。基本的数据类型: Number 、Boolean、Undefined、String、Null。

1
2
var  a = 1   //a是一个基本类型
var b = {m: 20 } //b是一个对象

对应内存存储:
在这里插入图片描述
当我们执行 b={m:30}时,堆内存就有新的对象{m:30},栈内存的b指向新的空间地址( 指向{m:30} ),而堆内存中原来的{m:20}就会被程序引擎垃圾回收掉,节约内存空间。我们知道js函数也是对象,它也是在堆与栈内存中存储的,我们来看一下转化:

1
2
3
4
5
6
7
8
9
var a = 1;
function fn(){
var b = 2;
function fn1(){
console.log(b);
}
fn1();
}
fn();

在这里插入图片描述
栈是一种先进后出的数据结构:
1 在执行fn前,此时我们在全局执行环境(浏览器就是window作用域),全局作用域里有个变量a;

2 进入fn,此时栈内存就会push一个fn的执行环境,这个环境里有变量b和函数对象fn1,这里可以访问自身执行环境和全局执行环境所定义的变量

3 进入fn1,此时栈内存就会push 一个fn1的执行环境,这里面没有定义其他变量,但是我们可以访问到fn和全局执行环境里面的变量,因为程序在访问变量时,是向底层栈一个个找,如果找到全局执行环境里都没有对应变量,则程序抛出underfined的错误。

4 随着fn1()执行完毕,fn1的执行环境被杯销毁,接着执行完fn(),fn的执行环境也会被销毁,只剩全局的执行环境下,现在没有b变量,和fn1函数对象了,只有a 和 fn(函数声明作用域是window下)


在函数内访问某个变量是根据函数作用域链来判断变量是否存在的,而函数作用域链是程序根据函数所在的执行环境栈来初始化的,所以上面的例子,我们在fn1里面打印变量b,根据fn1的作用域链的找到对应fn执行环境下的变量b。所以当程序在调用某个函数时,做了一下的工作:准备执行环境,初始函数作用域链和arguments参数对象


我们现在看回最初的例子outer与inner

1
2
3
4
5
6
7
8
9
function outer() {
var a = '变量1'
var inner = function () {
console.info(a)
}
return inner // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
}
var inner = outer() // 获得inner闭包函数
inner() //"变量1"

当程序执行完var inner = outer(),其实outer的执行环境并没有被销毁,因为他里面的变量a仍然被被inner的函数作用域链所引用,当程序执行完inner(), 这时候,inner和outer的执行环境才会被销毁调;《JavaScript高级编程》书中建议:由于闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内容,过度使用闭包,会导致内存占用过多。


js闭包的各种坑

坑点1: 引用的变量可能发生变化

1
2
3
4
5
6
7
8
9
function outer() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
};
}
return result;
}

看样子result每个闭包函数对打印对应数字,1,2,3,4,…,10, 实际不是,因为每个闭包函数访问变量i是outer执行环境下的变量i,随着循环的结束,i已经变成10了,所以每个函数内部的 i 值都是10,
怎么解决这个问题呢?

1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = (function(num) {
return function() {
return num; // 此时访问的num,是上层函数执行环境的num,数组有10个函数对象,每个对象的执行环境下的number都不一样
}();
})(i);
}
return result
}
console.log(outer()); //0,1,2,3,4,5,6,7,8,9

`我们没有直接把闭包赋值给数组,而是定义一个匿名函数,并立即执行该匿名函数的结果赋值给数组。我们在调用匿名函数时,我们传入的变量 i 。由于函数是按值传递的,所以将变量 i 的当前值复制给num。而这个匿名函数的内部,又创建并返回一个访问num的闭包。这样一来result数组中的每个函数都有自己的num变量的一个副本,因此返回不同的数值。


坑点2: this指向问题

1
2
3
4
5
6
7
8
9
10
var name = "The Window";
var object = {
name: "My Object",
getNameFuce: function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFuce()()); // The Window

由于getNameFuce()返回的是一个函数,因此调用object.getNameFuce()()就会立即调用它返回的函数,所以它返回的函数就相当于Window调用的,结果就返回一个全局的字符串The Window”。
因为里面的闭包函数是在window作用域下执行的,也就是说,this指向windows


this和arguments。内部函数在搜索这两个对象时,只会搜索到其活动对象为止,因此永远不能直接访问外部函数中的这两个变量。如果想访问作用域中的 this 和 arguments 对象,必须将该对象的引用保存到另一个闭包能够访问的变量之中。

我们可以把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name: "My Object",
getNameFuce: function() {
var that = this;
return function() {
return that.name;
};
}
};
console.log(object.getNameFuce()()); // My Object

在定义匿名函数之前,我们把 this 对象赋值给一个名叫that的变量。而定义了闭包之后,闭包可以访问这变量,因为它是我们在包含函数中特意声明的变量。即使在函数返回之后,that 也仍然引用着 object ,所以调用object.getNameFuce()() 就返回了 “My Object”。

下面几种特殊情况,this 的值也可能发生意外的改变。

1
2
3
4
5
6
7
8
9
10
var name = "The Window";
object = {
name: "My Object",
getName: function() {
return this.name;
}
};
console.log(object.getName()); // My Object
console.log((object.getName)()); // My Object
console.log((object.getName = object.getName)()); // The Window 非严格模式下

第一行是代码正常调用。第二行(object.getName)()调用 和object.getName() 是一样的。我们看一下第三行,先执行一条赋值语句,再调用赋值后的结果。因为这个赋值表达式的值是函数本身所有 this 的值不能得到维持,结果就返回了”The Window”。
当然我们在开发的时候,不会用第二行和第三行代码,不过,这个例子有助于说明即使是语法的细微变化,都有可能意外的改变 this 值。


坑点3:内存泄露问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function  showId() {
var el = document.getElementById("app")
el.onclick = function(){
aler(el.id) // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
}
}

// 改成下面
function showId() {
var el = document.getElementById("app")
var id = el.id
el.onclick = function(){
aler(id)
}
el = null // 主动释放el
}

闭包会引用包含函数的整个活动对象,而其中包含着el。即使闭包不直接引用el,包含函数的活动对象中也会保存一个引用,因此有必要吧el变量设置为null。


技巧1: 用闭包解决递归调用问题(当然现在大多用es6的let 和const 定义)

1
2
3
4
5
6
7
8
9
10
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1); //会产生耦合
}
}
var f = factorial;
factorial = null;
console.log(f(3)); //报错 1.html:2030 Uncaught TypeError: factorial is not a function

以上代码先把 factorial() 函数保存到变量 f 中,然后将 factorial 变量设置为null。结果指向原始函数的引用就只有一个 f ,但接下来调用 f() 时,由于必须执行 factorial() ,而 factorial 已经不再是函数了,所以就会导致错误,在这种情况下可以使用 arguments.callee() 可以解决这个问题。

1
2
3
4
5
6
7
8
9
10
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
var f = factorial;
factorial = null;
console.log(f(3)); //6

arguments.callee 是一个指向正在执行的函数的指针。但是在严格模式下,不能通过脚本访问 arguments.callee ,我们还可以用闭包的方式来解决。

1
2
3
4
5
6
7
8
9
10
var factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
var f = factorial;
factorial = null;
console.log(f(3)); //6

这样就没有问题了,实际上起作用的是闭包函数f,而不是外面的函数newFactorial,这样在严格模式和非严格模式都能使用。


技巧2:用闭包模仿块级作用域

es6没出来之前,用var定义变量存在变量提升问题

1
2
3
(function(){
//这里是块级作用域
})();

这是立即调用一个匿名函数


1
2
3
var someFunction = function(){
//这里是块级作用域
}

先定义一个函数然后再立即调用它,定义函数的方式是创建一个匿名函数,并把匿名函数赋值给变量someFunction 。
那么是不是也可以用函数的值直接取代函数名呢?然而下面的代码会导致错误。

1
2
3
function() {
//这里是块级作用域
}(); //报错

这段代码会导致语法错误,是因为JavaScript将function 关键字作为一个函数声明的开始,而函数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号,要将函数声明转化为函数表达式,加上圆括号即可。


无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:

1
2
3
4
5
6
7
8
9
function outputNumber(count) {
(function() {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 报错 Uncaught ReferenceError: i is not defined
}
outputNumber(10);

总结

总结一下闭包的好处与坏处

好处

①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
③匿名自执行函数可以减少内存消耗

坏处

①其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
②其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响



愿你的坚持终有收获。