blog icon indicating copy to clipboard operation
blog copied to clipboard

作用域和Closure ②

Open nanyang24 opened this issue 6 years ago • 0 comments

作用域和Closure ②

第一节,理解了closure的含义之后,我们继续探讨 scope 和 closure 的关系。

编写代码:closure 与 scope

过去,不了解JS中的 closure,当你在for循环中调用函数的时候,就很容易发生意想不到的结果。

来看下面这个例子:

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

如果你执行下就会发现,得到 5个 数字6。

  1. 首先,6是for循环终止后i的值。并且,即便我们给setTimeout的第二个参数传递0,它的回调函数timer也同样会在for循环结束之后才执行
  2. 其次,for 循环在JS中是没有自己作用域的。因此,循环变量i实际上是定义在全局作用域的变量
  3. 第三,每次循环,timer通过 closure 捕获的都是同一个全局变量 i

所以最终打印出来的值 是同一个全局变量 i 的 值:6

解决方案

那么如何解决这个看似复杂的问题呢?

其实,最关键的一点就是,不要让所有的 timer 都捕获同一个变量 i,而是让它们捕获当前迭代时 i 的一个拷贝。有了这个思路后,我们一步步来实现它。

IIFE 方案

我们让timer在每一次迭代的时候,捕获自己的作用域,而不是捕获全局作用域,这可以通过 IIFE (立即执行函数)实现:

for (var i = 1; i <= 10; i++) {
    (function() {
        // Scope begin
        setTimeout(function timer() {
            console.log(i);
        }, 0);
        // Scope close
    })();
}

这样,每一次迭代,timer 捕获到的,就是 // Scope begin 和 // Scope close 之间的作用域了。但这还不够,因为JS不会捕获一个空的作用域。

所以,我们在 timer 捕获的作用域里,保存一份当前 i 的拷贝:

for (var i = 1; i <= 5; i++) {
    (function() {
        // Scope begin
        var j = i;

        setTimeout(function timer() {
            console.log(j);
        }, 0);
        // Scope close
    })();
}

现在重新执行一下,就可以看到一开始期待的 12345 了。当然,我们也可以把上面的代码写成这样:

for (var i = 1; i <= 5; i++) {
    (function(j) {
        // Scope begin
        setTimeout(function timer() {
            console.log(j);
        }, 0);
        // Scope close
    })(i);
}

道理是一样的,函数的参数,实际上也是一个函数的临时变量。

ES6方案

上面的方案,有种为了解决普世问题而引发了不必要的代码。

在ECMAScript 6里,引入了一种在块作用域(block scope)中定义变量的方法:

for (var i = 1; i <= 5; i++) {
    let j = i;

    setTimeout(function timer() {
        console.log(j);
    }, 0);
}

用let定义的变量,更符合我们在其他编程语种的直觉,每一次迭代,都会有一个全新的变量 j,这样,timer 每次捕获的,就是当前 i 的拷贝了。

当然,既然 let 定义的变量有这个特性,我们可以直接用它来定义for循环,这就和我们印象中的for几乎一样了:

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

重新执行一下,结果仍旧是 12345

nanyang24 avatar Apr 30 '18 14:04 nanyang24