闭包的概念与应用

什么是闭包?

作为一个 JavaScript 语言的开发者,提起闭包肯定不会感到陌生,那么到底什么才是闭包哪?

闭包不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包的简称。对闭包的具体定义有很多种说法,这些说法大体可以分为两类:

  • 一种说法认为闭包是符合一定条件的函数。认为闭包是在其词法上下文中引用了自由变量(自由变量是指局部变量以外的变量)的函数。
  • 另一种说法认为闭包是函数和与其相关的引用环境组合而成的实体。认为闭包是在实现深约束时,需要创建一个能显示表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。

这两种定义在某种意义上是对立的,一个认为闭包是函数,另一个认为闭包是函数和引用环境组成的整体。很明显第二种说法更确切一些,闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。

JavaScript 闭包的本质

在支持嵌套作用域的语言中,有时不能简单直接的确定函数的引用环境。这样的语言一般具有这样的特性:

  • 函数是一等公民,即函数可以作为一个函数的返回值或参数,还可以作为一个变量的值
  • 函数可以嵌套定义,即在一个函数内部可以定义另一个函数。

JavaScript 闭包的源自两点,词法作用域和函数当做值传递

作用域是查找变量时的一些规则。词法作用域就是定义在词法阶段的作用域。或者换句话说,词法作用域是由你书写代码时将变量和块作用域写在哪里来决定的。按照代码书写时的样子,内部函数可以顺着作用域链一层一层地查找、访问函数外的变量,或者我们叫它自由变量。

函数当做值传递,也就是上面所说的函数是一等公民。函数内部的自由变量是在外层函数执行时创建的,外层函数执行完以后,这些变量理应被销毁,但是如果将内层函数作为返回值返回,这些自由变量就被保存了下来。而且无法访问,必须通过内层函数来访问。本来执行过程和词法作用域是封闭的,将内层函数作为返回值返回就提供了一种访问自由变量的方式。

一个函数如何能封闭外部状态哪?当外部状态的scope失效的时候,还有一份留在内部状态里面。在执行过程中,返回函数,或者将函数得以保留下来,并且函数中有自由变量就会形成闭包。一个函数中没有自由变量时,引用环境不会发生变化

闭包的应用

知道了什么是闭包,也理解了闭包的本质,下面可以了解下闭包的几种应用,或许你在日常的开发中已经用到不少了。

封装私有变量,存储计算的值

1
2
3
4
5
6
7
8
// 将计算的结果保存在 sum 中
function add(init) {
var sum = init;
return function getSum(number) {
sum += number;
return sum;
}
}

延迟计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 延迟计算
function add(init) {
var sum = init;
var args = [];
return function getSum() {
// 当参数到达一定的数量时再进行运算
args = args.concat(Array.from(arguments));
if(args.length > 5) {
for(let i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
}
}

延续局部变量的寿命

img 对象经常用于进行数据上报,但是通过查询后台的记录可以得知,因为一些低版本的浏览器的实现可能存在 bug,在这些浏览器下使用 report 函数进行数据上报会丢失 30% 左右的数据,也就是说,report 函数并不是每一次都发起了 HTTP 请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数调用结束后, img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这种方法会丢失 30% 左右的数据
var report = function (src) {
var img = new Image();
img.src = src;
};

// 把 img 变量封装起来,就可以解决请求丢失的问题
var report = (function(){
var imgs = [];
return function(src) {
var img = new Image();
imgs.push(img);
img.src = src;
}
})();

私有数据和应用程序接口

有时,你想强制程序与数据的交互方式,以便保护其完整性。通过是使用闭包,完全可以做到这一点。创建此类接口的一种常见方法就是从函数返回对象。这时,定义在原函数中的数据只能由返回对象上定义的方法访问,下面是一个例子:

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
function makeCalendar(name) {
var calendar = {
owner: name,
events: [],
};

return {
addEvent: function(event, dateString) {
var eventInfo = {
event: event,
date: new Date(dateString),
};
calendar.events.push(eventInfo);
calendar.events.sort(function(a, b) {
return a.date - b.date;
});
},

listEvents: function() {
if (calendar.events.length > 0) {
console.log(calendar.owner + "'s events are: ");

calendar.events.forEach(function(eventInfo) {
var dateStr = eventInfo.date.toLocaleDateString();
var description = dateStr + ": " + eventInfo.event;

console.log(description);
});
} else {
console.log(calendar.owner + " has no events.");
}
},
};
}

闭包与内存管理

局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就会一直存在。在这个意义上看,闭包的确会使一些数据无法被及时销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的。如果在将来需要回收这些变量的时候,可以手动把这些变量设置为 null。

参考内容

Powered by Hexo and Hexo-theme-hiker

Copyright © 2018 - 2020 阿母工业前端组 All Rights Reserved.

UV : | PV :