JS 闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包的效果

函数bar的词法作用域可以访问foo的词法作用域。在foo执行后,通常会期待foo的整个内部作用域被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。而闭包可以阻止这件事情的发生。

bar所声明的位置所赐,它拥有涵盖foo内部作用域的闭包使得该作用域能够一直存活,以供bar在之后任何时间进行引用。

bar依然持有对该作用域的引用,这个引用就叫做闭包

本质上无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

function wait(message) {
    setTimeout(() => console.log(message), 1000);
}
wait("Hello Closure"); 

将一个匿名函数传递给setTimeout,内部函数具有涵盖wait作用域的闭包,因此保留对变量message的引用。

循环与闭包

for (var i = 0; i < 5; ++i) {
    setTimeout(() => console.log(i), i * 1000);
}

正常情况下,我们对这段代码的预期是分别输出0-4,每秒一次,一次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次5。

5从哪里来?循环终止条件是i < 5, 条件首次成立时i的值为5,因此,输出显示的是循环结束时i的最终值。

仔细想一下,延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(…, 0),所有的回调函数依然是在循环结束后才会被执行。

为什么代码的行为跟语义所暗示的不一致?

我们试图假设循环中的每个迭代在运行时都会给自己捕获一个i的副本,但根据作用域的工作原理,实际情况是尽管循环中五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i

缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

IIFE通过声明并立即执行一个函数来创建作用域。

for (var i = 0; i < 5; ++i) {
    (function(j) { // IIFE需要有自己的变量在每次迭代中存储i的值
        setTimeout(function timer() { console.log(j); }, j * 1000); 
    })(i);
}

在迭代中使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

块作用域

我们使用IIFE在每个迭代都生成一个新的作用域,换句话说,每次迭代都需要一个块作用域。let声明可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

for (var i = 0; i < 5; ++i) {
    let j = i;
    setTimeout(function timer() {
        console.log(j);
    }, j * 1000);
}

for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不只被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

for (let i = 0; i < 5; ++i) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注