【每日面试题】什么是闭包,以及闭包的作用
定义
当 函数 可以 记住 并 访问 所在的词法作用域时,就 产生了闭包,即使 函数 是在 当前词法作用域之外 执行。(《你不知道的JavaScript - 上册》)
function foo() {
var a = 2;
function bar () {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 输出 2 ---- 这就是闭包的效果
// baz = function bar() { console.log(2);}
// 在全局作用域下,依然访问到了 foo 内部作用域中的 a;
baz 被赋值为 foo() 正常执行后的返回值,也就是 bar() 。调用 baz() 也就是通过不同的标识符(函数名)引用调用了 foo() 内部的函数 bar();
bar() 显然被正常执行了,但是它在自己 被定义的词法作用域以外 的地方 执行。
在 foo() 执行后,通常它的整个内部作用域都会被销毁,因为引擎的垃圾回收机制用来释放不再使用的内存空间。由于 foo() 的内容 看上去不会被再次使用,所以自然的会考虑对其进行回收。
然而闭包阻止这件事情发生,因为 bar() 还在使用 foo() 的内部作用域,所以它没有被回收。因为 bar() 的声明位置,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar 函数 在任何时间进行引用。
bar() 依然持有对该作用域的引用,而这种引用就是 闭包。
所以 baz 被实际调用后依然可以 foo 访问定义时的词法作用域,也可以访问变量 a。
这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
// demo1
function foo() {
var a = 2;
function baz() {
console.log(a); // 2
}
bar(baz);
}
function baz(fn) {
fn(); // 2 ---- 闭包
}
// demo2
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a); // 2
}
fn = baz; // baz 赋值给全局作用域的 fn
}
function bar () {
fn() // 闭包
}
foo();
bar(); // 2
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
function wait(message) {
setTimeout(function timer () {
console.log(message); // hello, i'm sanfen
}, 1000)
}
wait("hello, i'm sanfen");
本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型,并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是是在使用闭包。
循环和闭包
// 常见面试题
for (var i = 0; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
// 输出结果 6 个 6
输出结果为 6 的原因
这个循环的终止条件是 i <= 5,条件首次成立时 i 的值是6,所以输出显示的是 循环结束时的 i 的最终值。
延迟函数的回调函数会在循环结束后执行,即使 setTimeout(..., 0),所有的回调函数还是会在循环结束后才执行,所以才会每次都输出一个6出来。
// 使用匿名函数进行修改
for (var i = 0; i <= 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j)
}, i * 1000)
})(i)
}
匿名函数每次迭代都会生成一个新的作用域,是的延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代都会有新的正确的值供我们访问。
重返块作用域
// 使用块级作用域进行修改
for (let i = 0; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
使用 let 块级作用域,每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
模块
function CoolModule() {
var something = 'cool';
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join('!'));
}
return {
doSomething: doSomething,
doAnother: doAnother,
}
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
这个模式在 JS 中称之为模块。最常见的实现模块模式的方法通常被称为模块暴露。
doSomething 和 doAnother 函数具有涵盖模块实例内部作用域的闭包(通过调用CoolModule() 实现)。当通过返回一个含有属性引用的对象的方式将函数传递到词法作用域外部时,我们已经创造了可以观察和实践闭包的条件。
模块的两个必要条件
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。
闭包的作用
- 变量私有化,避免全局污染
- 延续局部变量的寿命 (也可能变成缺点)
- 模块模式
闭包的缺点
- 导致变量不会被垃圾回收机制回收,造成内存消耗
- 解决需手动删除
- 不恰当的使用闭包可能会造成内存泄漏的问题