一、什么是闭包?
MDN对闭包的解释
关键词(函数、词法环境、引用、组合)
函数
ES5规范中JavaScript仅有全局作用域和函数作用域(ES6增加块级作用域),根据作用域链,函数内部的代码可以访问外部声明的变量,但是外部代码无法访问函数内部的变量。换句话说如果你定义了一个叫Outer的函数,在Outer函数内部又定义了一个Inner函数,那么inner函数就可以访问Outer函数中声明的变量。
词法环境
词法环境涉及到JavaScript的执行上下文,这里不展开,只需要知道每个函数都具有自己的执行上下文即可。在JS执行上下文中,就包含了词法环境和变量环境。
引用、组合
还是用Outer函数和Inner函数说明,当Inner函数有代码访问Outer函数声明的变量时,就形成了引用,那么Outer函数和Inner函数就是一个闭包组合。
二、闭包有什么作用?
- 突破作用域链,使得外部代码可以访问函数内部代码
- 被引用的变量在其定义的函数执行完毕后不会被回收,仍在内存中
图中看到f1函数把内部函数f2 return出去,保存在一个全局变量result中,此时,只要执行result函数,就能访问到f1函数中定义的局部变量n了,并且说明了f1函数执行完毕后,局部变量n没有被回收掉。
三、闭包的最佳利用
模拟对象的私有属性
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
用一个立即执行函数(IIFE)返回一个对象保存在Counter变量中,该对象向外部提供了3个方法来访问和操作对象的私有属性,increment和decrement方法通过changeBy闭包函数来修改私有属性privateCounter的值,value方法则用来获取privateCounter的值。这种设计保证了外部代码能正常使用该对象,同时又隐藏了对象内部的核心逻辑代码。
循环中常见错误
function fun() {
for (var i = 1; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
}
fun();
/* logs 5 */
/* logs 5 */
/* logs 5 */
/* logs 5 */
在fun函数中,用for循环了设定了4个定时器,0ms后执行,本来我们预期输出的是1、2、3、4,然而结果却是输出了4个5,这是因为for循环中用var声明的循环条件变量其实是fun函数的局部变量,定时器中的回调函数其实也是一个闭包函数,只是它们都共享fun函数的词法环境,等到回调函数执行log输出时,fun函数中变量i已经是5了。
function fun() {
for (var i = 1; i < 5; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 0);
})(i);//用立即执行函数(IIFE)创建闭包引用
}
}
fun();
/* logs 1 */
/* logs 2 */
/* logs 3 */
/* logs 4 */
现在我们修改一下fun函数,在每次for循环中都执行一个IIFE,目的就是为了创建闭包引用的独立词法环境(IFEE的词法环境),执行了4次IIFE就会创建4个闭包引用,每个闭包引用的词法环境独立不共享,因此实现了我们预期的输出结果1、2、3、4。当然,在ES6规范中,我们更适合用let声明for循环中的条件变量,利用块级作用域解决。
四、闭包的缺陷
- 闭包函数引用的变量一直保留在内存中,如果有多个闭包函数,势必造成占用内存过多而导致性能问题。
- 闭包调用完后不再使用,但没有清空引用闭包函数的变量,会造成内存泄漏。
\
本文为记录自己学习前端知识的个人理解总结,不保证理解正确到位,欢迎评论指出和纠正错误,欢迎一起学习前端。