要理解 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 行尝试打印输出变量 x,显然这个时候是会报错的,在第 16 行打一个断点调试就可以清楚的看到,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。
当然,这里指的是自动产生闭包的情况,关于闭包,有时需要根据需求手动的来制造一个闭包。
来看下面的例子:
function eat(){
var food = "鸡翅";
console.log(food);
}
eat(); // 鸡翅
console.log(food); // 报错
在上面的例子中,声明了一个名为 eat 的函数,并对它进行调用。
JavaScript 引擎会创建一个 eat 函数的执行上下文,其中声明 food 变量并赋值。
当该方法执行完后,上下文被销毁,food 变量也会跟着消失。这是因为 food 变量属于 eat 函数的局部变量,它作用于 eat 函数中,会随着 eat 的执行上下文创建而创建,销毁而销毁。所以当我们再次打印 food 变量时,就会报错,告诉我们该变量不存在。
但是我们将此代码稍作修改:
function eat(){
var food = '鸡翅';
return function(){
console.log(food);
}
}
var look = eat();
look(); // 鸡翅
look(); // 鸡翅
在这个例子中,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
最后,对闭包做一个总结:
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
在上面的代码中,预期的结果是过 1 秒后分别输出 i 变量的值为 1,2,3。但是,执行的结果是:4,4,4。
实际上,问题就出在闭包身上。循环中的 setTimeout 调用了一个匿名函数访问了它的外部变量 i,形成闭包。
而 i 变量只有 1 个,所以循环 3 次的 setTimeout 中都访问的是同一个变量。循环到第 4 次,i 变量增加到 4,不满足循环条件,循环结束,代码执行完后上下文结束。但是,那 3 个 setTimeout 等 1 秒钟后才执行,由于闭包的原因,所以它们仍然能访问到变量 i,不过此时 i 变量值已经是 4 了。
要解决这个问题,我们可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:
for (var i = 1; i <= 3; i++) {
(function (index) {
setTimeout(function () {
console.log(index);
}, 1000);
})(i)
}
这样 setTimeout 中就可以不用访问 for 循环声明的变量 i 了。而是采用调用函数传参的方式把变量 i 的值传给了 setTimeout,这样它们就不再创建闭包,因为在我自己的作用域里面能够找到 i 这个变量。
当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字。
它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解。
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
使用闭包可以解决一个全局变量污染的问题。
如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。
以上笔记整理于渡一教育谢老师课堂