前言
前几天听到同学说在面试被问到闭包的场景,他说:"既然闭包有可能造成内存泄漏,那还用它干嘛,不就没事找事做吗",我本着存在即合理给他解释...
你有没有想过,其实你并没有那么了解闭包,在面试中被问到:"讲一下你了解的闭包",你是否还只会说闭包 = 内层函数使用外层函数变量,由于变量无法回收,所以在函数外部也能使用该变量...
接下来,我们一层一层将闭包解剖,看看闭包到底有什么神奇之处
闭包到底是什么?
通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),通过闭包实现了能 在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。所以可以说闭包是外部使用函数内部的工具。
举个例子:
js
function fn1() {
var a = 1;
return function(){
console.log(a);
};
}
var res = fn1();
res(); // 1
结合闭包的概念,我们执行这段代码,就可以发现最后输出的结果是 1(即 a 变量的值)。那么可以很清楚地发现,a 变量作为一个 fn1 函数的内部变量,正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,我们最后还是可以拿到 a 变量的值。
闭包是如何产生的?
闭包产生的原因跟作用域息息相关,所以你还需要知道什么是作用域链。其实很简单,当访问一个变量时,代码解释器会在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。
每个子函数都会拷贝上级的作用域,形成作用域链。我们还是用上述代码来解释:
js
function fn1() {
var a = 1;
return function fn2(){
console.log(a);
};
}
var res = fn1();
res(); // 1
- 通过作用域的概念,fn1 函数的作用域指向全局作用域(window)和它自己本身;fn2 函数的作用域指向全局作用域 (window)、fn1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。
- 那么这就很形象地说明了什么是作用域链,即当前函数一般都会保存上层函数的作用域的引用,从而形成一条作用域链。
- 由此可见,
闭包产生的本质就是:当前环境中存在指向父级作用域的引用
。
这也就是为什么上述代码能够正确打印出 a 变量的值的原因。因为在当前环境中存在 fn2 -> fn1 -> 全局的作用域链,所以能够找到 a 变量。
是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,我们只需要让父级作用域的引用存在即可,所以也可以将代码修改为:
js
var res;
function fn1() {
var a = 1;
res = function(){
console.log(a);
};
}
fn1();
res(); // 1
可以看到输出的结果并没有改变,所以只要当前环境仍然存在指向某个作用域的引用,那就可以获取到该作用域上的变量。
闭包的用途
-
在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
-
使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
众所周知,原始数据类型是保存在栈空间中的,在上述函数中 fn1 销毁的情况下,我们如何访问到 a 变量的值的呢?
js
function fn1() {
var a = 1;
return function fn2(){
console.log(a);
};
}
fn1();
var res = fn1();
console.log(res.prototype);
我们将 res.prototype 打印出来:
上图红框的位置我们能看到一个内部的对象 [[Scopes]]
,其中存放着变量 a,该对象是被存放在堆上的,其中包含了闭包、全局对象等等内容,因此我们能通过闭包访问到本该销毁的变量。
处处可见的闭包
在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
举个比较经典的例子:
js
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
上述代码执行后,答案是连续输出 5 个 6,一道很经典的面试题,如何解释能让面试官比较满意呢:
setTimeout
作为宏任务,由于 JS 中单线程eventLoop 机制
,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行
- 因为
setTimeout
函数也是一种闭包,往上找它的父级作用域链就是 window
,变量 i 为 window 上的全局变量
,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。
模拟真实场景
需求:输入框,根据输入的内容及时发送请求(请求下拉框的数据)
简陋的封装一下:
js
<div><input type="text"></div>
const ip = document.querySelector('input');
ip.addEventListener('input', (e) => {
// 使用 setTimeout 模拟请求
setTimeout(() => {
console.log(e.target.value);
}, 0)
})
如图:

这时我们将输入 1 的时候做一个延迟效果,模拟第一个数据包最慢响应的场景
js
ip.addEventListener('input', (e) => {
if(e.target.value == 1) {
setTimeout(() => {
console.log(1);
}, 1000);
}
else {
setTimeout(() => {
console.log(e.target.value);
}, 0)
}
})
如图:

在这种情况下,下拉框展示的应该是发送 1 的时候的数据,而不是 111 的数据,应该如何改变呢?是不是就可以利用闭包的特性来修复这个bug:
js
ip.addEventListener('input', (e) => {
const queryData = e.target.value;
if(e.target.value == 1) {
setTimeout(() => {
if(e.target.value == queryData) console.log(queryData);
}, 1000);
}
else {
setTimeout(() => {
if(e.target.value == queryData) console.log(queryData);
}, 0)
}
})
效果如图:

当然真实的场景可能会复杂得多,但是充分利用好闭包的特性,也许能够帮助你解决很多问题。
总结
本篇主要从 什么是闭包 -> 产生的原因 -> 用途 -> 实际场景 介绍了闭包,也希望能够帮助大家了解闭包,有什么问题也请及时指出。