携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
对学过的知识进行二次学习,不仅能够复习学过的知识,还可能有不可思议的收获。
什么是闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
下面我们通过几个例子,特别注意例子下面的说明,能帮助你更好理解闭包。
闭包一:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 --- 闭包
- 函数 bar() 的词法作用域能够访问 foo() 的内部作用域。
- foo() 执行后,不会被垃圾回收器释放不再使用的内存空间,因为 bar() 声明处引用了 foo() 内部作用域,因此没有被回收,这就是 闭包 的一个特点。
- bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。因此, baz 可以访问定义时的词法作用域,它也可以访问变量 a。
闭包二:
function foo() {
const a = 2
function baz() {
console.log(a)
}
bar(baz)
}
function bar(fn) {
fn() // 闭包
}
这跟上个例子一样,只不过把内部函数 baz 作为参数传递给 bar(fn),在 bar(fn) 内部引用了 foo 内部作用域,这也是闭包。
传递函数当然也可以是间接的。
闭包三:
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a)
}
fn = baz // 将 baz 分配给全局变量
}
function bar() {
fn() // 闭包
}
foo()
bar() // 2
可能你也写过这种代码:
function log(message) {
setTimeout(function timer() {
console.log(message)
}, 1000)
}
log('hello world')
- timer 具有 log() 作用域的闭包,还引用了变量 message。
- log() 执行1000毫秒后,它的内部作用域并不会消失,timer 仍然对 log() 作用域引用。
循环与闭包
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
6 6 6 6 6
我们预期打印的是 1-5,但是它输出了 5 个 6,这是因为 setTimeout 函数的回调会在循环结束之后才执行。
我们需要在循环的过程中每次迭代创建一个闭包作用域
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
1 2 3 4 5
其实原理就是使用了 IIFE (立即执行函数) 在每次迭代的时候创建一个新的作用域。
let 声明可以用来创建块级作用域。
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
1 2 3 4 5
闭包注意
从上文我们可以看出,闭包引用了本该被垃圾回收机制回收的函数作用域,使其不被回收,这样一来,就导致了内存泄漏。
如果想避免的话,在使用完变量时候,应当将其赋值为 null,比如以上的 闭包三 的例子:
function bar() {
fn() // 闭包
fn = null
}
fn = null 将其引用去除,垃圾回收机制便可重新将其回收,避免内存泄漏问题。