概览
背景知识:JavaScript内存管理、JavaScript作用域。
内容
1 闭包定义
闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var fn = foo(); // parent函数不会被垃圾回收
fn() // 2
细节:1 bar函数定义在foo函数内部,bar函数本身被当作一个值类型进行传递,并且它在定义的词法作用域之外执行。
2 foo()执行完后不会被垃圾回收,bar()将一直持有对该作用域的引用,而这个引用就是闭包。
以下依然是闭包:
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
fn(); // 闭包
}
总结:无论通过何种手段将内部函数传递到所在的词法作用域外,它都会持有对原始定义词法作用域的引用,无论在何处执行这个函数都会使用到闭包。
闭包 定时器:
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait();
分析:wait()执行1000ms后,它不会被垃圾回收,timer()依然wait()作用域的闭包。
闭包 事件监听器:
function addEvent(selector) {
$('selector').click(function cb() {
// todo
})
}
addEvent(btn);
谨记:在定时器、事件监听器、Ajax请求、跨窗口通信、WebWorkers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是使用闭包。
循环与闭包
块级作用域与闭包的结合功能强大。
内容
思考:
for (var i = 0; i < 5; i++) {
setTimeout(function Timer() {
console.log(i);
});
}
分析:尽管循环中的函数是在各个迭代中分别定义,但是它们都被封闭在共享的全局作用域中,实际上只有一个i。
1 利用IIFE:
for(var i = 0; i < 5; i++) {
(function print() {
var j = i;
setTimeout(function() {
console.log(j)
}, j*1000);
})();
}
2 利用let、const:
for(let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
3 模块
模块也是一种利用闭包的代码模式。
模块模式的两个必要条件
1 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问和修改私有的状态。
一个具有函数属性的对象本身并不是真正的模块。
一个从函数调用返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。