跳到主要内容

使用闭包

闭包( closure )是一个函数,通常也被称为闭包函数或绑定函数,该函数运行在一个特定的环境中,该环境中定义了一些本地变量,当该函数被调用时,仍可以使用这些本地变量。内部函数可以访问外部函数的私有成员。若内部函数引用了外部函数的私有成员,同时内部函数又被传给了外界,或对外界开放,那么闭包就形成了。

认知闭包函数 ,就是嵌套结构的函数 ,在一个函数内定义的一个函数 . 作为闭包的必要函数 , 内部函数应该访问外部函数中声明的私有变量、参数、其它内部函数。闭包步骤说明: 。

  • 预编译,开始解析,创建执行环境,创建调用对象,把参数和局部变量、内部的函数转换为对象属性
  • 执行函数体内代码
  • 外部函数把内部函数返回给全局变量,实现内部函数的定义。此时,被赋值 完全继承了内部函数 的所有结构和数据
  • 外部函数返回,自动销毁,内部的结构、标识符和数据也随之丢失
  • 执行代码程序

函数实际只是一代代吗,是静态文本,在调用前,仅是词法意思上的结构,没有实际的价值。包括在预编译函数时,也仅是简单的分析函数的词法、语法结构,并根据函数标识符占据内存空间,其内部结构和逻辑并没有被执行。而闭包是动态的。

函数闭包
功能设计逻辑结构储存和传输数据
状态静态词法域动态环境
时间即时性(调用返回后即消失)延迟性(在函数返回后,还能使用)
作用域根据词法作用域,可以事先确定作用域关系动态作用域,只在具体的环境中调用函数时,才能确定其作用域

如果简单描述,闭包可以说是函数的数据包,储存数据。这个数据在函数执行的过程中始终处于激活状态。当函数调用返回之后,闭包保持着与函数关联的变量的动态关系。

理解 JavaScript 中的闭包

例如,以下代码会输出 5次,结果都是 5,如何输出 0、 1、 2、 3、 4?。

for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}

利用闭包的原理实现,代码如下:

for (var i = 0; i < 5; i++) {
(function (e) {
setTimeout(function () {
console.log(e);
}, 1000);
})(i);
}

作为值从函数返回的函数是闭包函数

例如下面的代码,闭包函数 A()位于一个特定的环境中(被嵌套在另一个函数 B()中),并作为值从函数 B()返回:

function B() {
var temp = 'abc'; //这是一个本地变量
function A() {
// 定义一个闭包函数,将使用本地变量
//
alert('闭包函数处理本地变量 temp 的值 : ' + temp);
}
return A; // 返回闭包函数
}
var myFunc = B(); // 调用函数 B(),返回的是闭包函数 a()
myFunc(); //调用闭包函数 a();

注意, myFunc()调用是在函数 B()外边,按照常理,它不应该再访问到本地变量 temp ,但实际上仍可以访问,这就是闭包函数。

利用变量作用范围也可以形成闭包函数

一般来说,作为参数传递给函数的函数或作为值从函数返回的函数都是闭包函数,但是,利用变量作用范围也可以形成闭包函数,例如下面的代码:

var F;
function B() {
var temp = 'abc'; // 这是一个本地变量
F = function () {
//定义一个闭包函数,将使用本地变量
alert('闭包函数处理本地变量 temp 的值 : ' + temp);
};
}
B(); // 调用函数 B()为变量 F 赋值
F(); // 调用闭包函数 F()

可以看到,闭包函数 F()的调用虽不在函数 B()的局部作用范围内,但仍可以使用函数 B()中定义的私有变量 temp ,这就是闭包。

闭包的核心乃是函数无论在哪里调用,仍可以访问它所处环境的变量,而这个变量在函数被调用的环境中是被其它程序访问不到的。

匿名自执行函数是非常好的利用闭包原理来实现的功能应用,使用 get 和 set 存取器方法定义属性也是一个很好的闭包原理应用。

闭包函数常用的环境

一个比较常见的应用是在执行函数之前就向函数传递参数,例如下面的代码:

function outer(param) {
// 返回一个匿名函数
return function () {
alert(param);
};
}
// 调用外部函数,返回对匿名函数的引用
var callback = outer('abc'); //每隔 1000毫秒就执行一次匿名函数
setTimeout(callback, 1000);

注意,每隔 1000毫秒执行一次内部的匿名函数,但是却没有向匿名函数传递新的参数。因为作为值从函数返回的函数都是闭包函数,因此,每次都能输出外部函数的参数 param 的值。

易犯的错误

例如下面的代码,一般会认为返回 1~9这 10个数字,但是实际会输出 10次数字 10,因为匿名函数中的 i 实际是在一个闭包环境下,当执行第一个 setTimeout 之前, for 循环肯定已经完成了, i 的值现在是 10,因此所有的 alert(i)都会是 alert(10):

for (var i = 0; i < 10; i++) {
setTimeout(function () {
alert(i);
}, 1000);
}

用户可以使用下面的代码解决这个问题,每次向外部的匿名函数传递一个值,实际是执行了 10个匿名函数,每一个匿名函数是一个作用范围,而 setTimeout 包含在每一个匿名函数内,因此,现在会返回 1~9这 10个数字:

for (var i = 0; i < 10; i++) {
(function (e) {
setTimeout(function () {
alert(e);
}, 1000);
})(i);
}

还可以使用下面的代码实现相同的功能,当 for 循环执行时,实际 setTimeout 的第一个参数已经执行,第一个参数的值是内嵌的一个匿名函数,这样当延迟执行时,实际上是执行的内嵌的匿名函数,此时该匿名函数内的变量 e 实际在每次循环时已经赋值了:

for (var i = 0; i < 10; i++) {
setTimeout(
(function (e) {
return function () {
alert(e);
};
// 返回一个匿名函数
})(i),
1000,
);
}

如果为内嵌的函数增加一个参数:

for (var i = 0; i < 10; i++) {
setTimeout(
(function (e) {
return function (e) {
alert(e);
};
// 返回一个匿名函数
})(i),
1000,
);
}

那么这实际会形成 10行如下的程序:


setTimeout(function(e){alert(e);}, 1000)
...

setTimeout(function(e){alert(e);}, 1000)

由于没有向函数调用传递参数,所以会输出 undefined ,即参数未定义。

内存泄漏

JavaScript 的解释器都具备垃圾回收机制,一般采用的是引用计数的形式。如果一个对象的引用计数为零,则垃圾回收机制会将其回收,这个过程是自动的。但是,有了闭包的概念之后,这个过程就变得复杂起来了,在闭包中,因为局部的变量可能在将来的某些时刻需要被使用,因此垃圾回收机制不会处理这些被外部引用到的局部变量,而如果出现循环引用,即对象 A 引用 B , B 引用 C ,而 C 又引用到 A ,这样的情况使得垃圾回收机制得出其引用计数不为零的结论,从而造成内存泄漏。

解决内存泄漏最简单的方法就是阻断内部函数对外部函数的变量引用,这样就形不成闭包。