在理解 JavaScript 中的闭包前先了解以下两个知识点:
简单回顾一下这两个知识点:
1. JavaScript 中的作用域和作用域链
2. JavaScript 中的垃圾回收
有了这 2 个知识点的铺垫后,接下来再看什么是闭包。
闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包。
而作用域链,正是实现闭包的手段。
真的是这样么?下面我们可以证明一下:

在上面的代码中,我们在函数 a 中定义了一个变量 i,然后打印这个 i 变量。对于 a 这个函数来讲,自己的函数作用域中存在 i 这个变量,所以我们在调试时可以看到 Local 中存在变量 i。
下面我们将上面的代码稍作修改,如下图:

在上面的代码中,我们将声明 i 这个变量的动作放到了 a 函数外面,也就是说 a 函数在自己的作用域已经找不到这个 i 变量了,它会怎么办?
它会顺着作用域链一层一层往外找。然而上面在介绍闭包时说过,如果出现了这种情况,也就是函数使用了外部的数据的情况,就会创建闭包。
观察调试区域,我们会发现此时的 i 就放在 Closure 里面的。
理解一下~
“闭”可以理解为“封闭,闭环”,“包”可以理解为“一个类似于包裹的空间”,因此闭包实际上可以看作是一个封闭的空间,那么这个空间用来干啥呢?实际上就是用来存储变量的。

一个函数下所有的变量声明都会被放入到闭包这个封闭的空间里面么?
倒也不是,放不放入到闭包中,要看其他地方有没有对这个变量进行引用,例如:

在上面的代码中,函数 c 中一个变量都没有创建,却要打印 i、j、k 和 x,这些变量分别存在于 a、b 函数以及全局作用域中,因此创建了 3 个闭包,全局闭包里面存储了 i 的值,闭包 a 中存储了变量 j 和 k 的值,闭包 b 中存储了变量 x 的值。
观察发现函数 b 中的 y 变量并没有被放在闭包中,所以要不要放入闭包取决于该变量有没有被引用。
问题来了,那么多闭包,那岂不是占用很多内存空间么?
实际上,如果是自动形成的闭包,是会被销毁掉的。例如:

在上面的代码中,我们在第 16 行尝试打印输出变量 k,显然这个时候是会报错的,在第 16 行打一个断点调试就可以清楚的看到,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。
当然,这里指的是自动产生闭包的情况,关于闭包,有时我们需要根据需求手动的来制造一个闭包。
示例:
function eat(){
var food = "鸡翅";
console.log(food);
}
eat(); // 鸡翅
console.log(food); // 报错
在这个例子中,eat 函数返回一个函数,并在这个内部函数中访问 food 这个局部变量。调用 eat 函数并将结果赋给 look 变量,这个 look 指向了 eat 函数中的内部函数,然后调用它,最终输出 food 的值。
为什么能访问到 food,原因很简单,上面我们说过,垃圾回收器只会回收没有被引用到的变量,但是一旦一个变量还被引用着的,垃圾回收器就不会回收此变量。在上面的示例中,照理说 eat 调用完毕 food 就应该被销毁掉,但是我们向外部返回了 eat 内部的匿名函数,而这个匿名函数有引用了 food,所以垃圾回收器是不会对其进行回收的,这也是为什么在外面调用这个匿名函数时,仍然能够打印出 food 变量的值。
闭包的特点:
利用这一特性,可以使用“闭包”解决全局变量污染的问题。早期在 JavaScript 还无法进行模块化的时候,这是一个“好”办法。
例如:
var name = "GlobalName";
// 全局变量
var init = (function () {
var name = "initName";
function callName() {
console.log(name);
// 打印 name
}
return function () {
callName();
// 形成接口
}
}());
init(); // initName
var initSuper = (function () {
var name = "initSuperName";
function callName() {
console.log(name);
// 打印 name
}
return function () {
callName();
// 形成接口
}
}());
initSuper(); // initSuperName