logo
Published on

JavaScript 闭包与循环 - 简单实用示例

Authors
  • Name
    Twitter

最近在研究JavaScript的闭包问题时,发现许多答案没有完全解释JavaScript如何处理作用域,这实际上是问题的本质。

大多数人提到的问题是内部函数引用了相同的i变量。那为什么我们不在每次迭代时创建一个新的局部变量,并让内部函数引用它呢?

// 覆盖console.log()以便查看控制台输出
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; // 创建一个新的局部变量
    funcs[i] = function() {
        console.log("My value: " + ilocal); // 每个内部函数应引用其自己的局部变量
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

和之前一样,每个内部函数输出的是i的最后一个赋值,现在每个内部函数输出的却是ilocal的最后一个赋值。每次迭代不应该有自己的ilocal值吗?

实际上,这是问题的关键。每次迭代共享相同的作用域,所以在第一次迭代之后,每次迭代都会覆盖ilocal。如MDN所述:

JavaScript没有块级作用域。使用块引入的变量作用域包含在包含它的函数或脚本中,其设置效果超出了块本身。换句话说,块语句不会引入作用域。尽管“独立”块是有效语法,但你不想在JavaScript中使用独立块,因为如果你认为它们像C或Java中的块一样工作,那就不是你想的那样。

强调再强调:

JavaScript没有块级作用域。使用块引入的变量作用域包含在包含它的函数或脚本中。

我们可以通过在每次迭代中声明ilocal之前检查来看出这一点:

// 覆盖console.log()以便查看控制台输出
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    console.log(ilocal);
    var ilocal = i;
}

这正是这个问题如此棘手的原因。即使重新声明一个变量,JavaScript也不会抛出错误,JSLint甚至不会发出警告。这也是为什么解决这个问题的最佳方法是利用闭包,基本上就是指在JavaScript中,内部函数可以访问外部变量,因为内部作用域“封闭”了外部作用域。

Image 1: Closures

这也意味着内部函数“保留”外部变量并将其保活,即使外部函数返回也是如此。为利用这一点,我们创建并调用一个包装函数,仅仅是为了创建一个新的作用域,在新作用域中声明ilocal,并返回一个使用ilocal的内部函数:

// 覆盖console.log()以便查看控制台输出
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { // 使用包装函数创建新的作用域
        var ilocal = i; // 捕获i到局部变量中
        return function() { // 返回内部函数
            console.log("My value: " + ilocal);
        };
    })(); // 记得运行包装函数
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

在包装函数内创建内部函数为内部函数提供了一个只有它能访问的私有环境,即“闭包”。因此,每次调用包装函数时,我们都会创建一个新的具有自己独立环境的内部函数,确保ilocal变量不会相互冲突和覆盖。经过一些小的优化,得到了许多SO用户提供的最终答案:

// 覆盖console.log()以便查看控制台输出
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
// 为内部函数创建单独的环境
function wrapper(ilocal) {
    return function() { // 返回内部函数
        console.log("My value: " + ilocal);
    };
}

更新

随着ES6的普及,我们现在可以使用新的let关键词来创建块级作用域变量:

// 覆盖console.log()以便查看控制台输出
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // 使用"let"声明"i"
    funcs[i] = function() {
        console.log("My value: " + i); // 每个内部函数应引用其自己的局部变量
    };
}
for (var j = 0; j < 3; j++) { // 这里可以使用"var"而没有问题
    funcs[j]();
}