重学js-闭包

106 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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 将其引用去除,垃圾回收机制便可重新将其回收,避免内存泄漏问题。