跳到主要内容

Closure(闭包)

概念

Closure(闭包)

闭包是指有权访问另外一个函数作用域中的变量的函数。 ——红宝书

一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。 ——MDN

闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以

通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。

function fun1() {
var a = 1;
return function () {
console.log(a);
};
}
fun1();
var result = fun1();
result(); // 1

可以很清楚地发现,a 变量作为一个 fun1 函数的内部变量,正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,我们最后还是可以拿到 a 变量的值。

本质

函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员

特性

  • 函数嵌套
  • 函数内部可以引用外部的参数和变量
  • 参数和变量不会被垃圾回收机制回收

优点

  • 避免全局变量的污染

缺点

  • 常驻内存,增加了内存的使用量
  • 使用不当容易造成内存泄漏

应用场景: 缓存变量

闭包的表现形式

1. 返回一个函数。

2. 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

// 定时器
setTimeout(function handler(){
console.log('1');
}1000);
// 事件监听
$('#app').click(function(){
console.log('Event Listener');
});

3. 作为函数参数传递的形式。

var a = 1;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
// 这就是闭包
fn();
}
foo(); // 输出2,而不是1

4. IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量。

var a = 2;
(function IIFE() {
console.log(a); // 输出2
})();

IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域,我们经常能在高级的 JavaScript 编程中看见此类函数。

比较常见的开发应用场景

如何解决循环输出问题?
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}

上面这段代码执行之后,从控制台执行的结果可以看出来,结果输出的是 5 个 6,那么一般面试官都会先问为什么都是 6?我想让你实现输出 1、2、3、4、5 的话怎么办呢?

提示

setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。

因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

如何按顺序依次输出 1、2、3、4、5

可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行。

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