JavaScript:如何在setTimeout中使用可变全局变量

10

我正在使用Firefox Scratchpad执行Javascript,并想在我的setTimeout函数(或任何异步执行的函数)内获取全局索引。由于数据的顺序必须保持按顺序执行的方式,因此无法使用Array.push。以下是我的代码:

function Demo() {
    this.arr = [];
    this.counter = 0;
    this.setMember = function() {
        var self = this;

        for(; this.counter < 10; this.counter++){
            var index = this.counter;
            setTimeout(function(){
                self.arr[index] = 'I am John!';
            }, 100);
        }
    };
    this.logMember = function() {
        console.log(this.arr);
    };
}

var d = new Demo();
d.setMember();

setTimeout(function(){
    d.logMember();
}, 1000);

我想让我的 d.arr 数组有 0 - 9 的索引,每个索引都包含字符串 'I am John!',但只有第 9 个索引包含字符串 'I am John!'。我认为将 this.counter 保存到本地变量 index 中会对 this.counter 进行快照。请问有谁能帮我理解一下我的代码错在哪里吗?

4个回答

14

这种情况的问题与JS中的作用域有关。由于没有块级别作用域,它基本上等同于

this.setMember = function() {
    var self = this;
    var index;

    for(; this.counter < 10; this.counter++){
        index = this.counter;
        setTimeout(function(){
            self.arr[index] = 'I am John!';
        }, 100);
    }
};

当然,由于该任务是异步的,循环将一直运行到完成,将索引设置为9。然后该函数将在延迟100ms后执行10次。

有几种方法可以做到这一点:

  1. IIFE(立即调用的函数表达式)+闭包

this.setMember = function() {
    var self = this;
    var index;

    for(; this.counter < 10; this.counter++){
        index = this.counter;
        setTimeout((function (i) {
            return function(){
                self.arr[i] = 'I am John!';
            }
        })(index), 100);
    }
};

在这里,我们创建了一个匿名函数,并立即使用索引调用它,然后返回一个执行赋值操作的函数。当前的index值在闭包作用域中保存为i,并且赋值操作是正确的。

  • 与1类似,但使用了一个单独的方法。

  • this.createAssignmentCallback = function (index) {
        var self = this;
        return function () {
             self.arr[index] = 'I am John!';
        };
    };
    
    this.setMember = function() {
        var self = this;
        var index;
    
        for(; this.counter < 10; this.counter++){
            index = this.counter;
            setTimeout(this.createAssignmentCallback(index), 100);
        }
    };  
    
  • 使用Function.prototype.bind

  • this.setMember = function() {
        for(; this.counter < 10; this.counter++){
            setTimeout(function(i){
                this.arr[i] = 'I am John!';
            }.bind(this, this.counter), 100);
        }
    };
    

    由于我们只关心如何将正确类型的i传递到函数中,因此可以利用bind的第二个参数进行部分应用,以确保稍后将使用当前索引调用函数。由于可以直接绑定调用函数的this值,因此还可以摆脱self = this这一行。当然,我们也可以摆脱索引变量并直接使用this.counter,使其更加简洁。

    就我个人而言,我认为第三种解决方案是最好的。 它简短、优雅,并且正好能够满足我们的需求。 其他一切都是更多的hack,以完成语言当时不支持的功能。 由于我们有了bind,因此没有比这更好的解决方法。


    如果我有除了索引以外的两个参数(我不应该传递)怎么办?例如:function(result, status)。那么我们如何使用bind呢? - Prakhar Mishra
    你基本上可以像使用 Function.prototype.call 一样使用它。所以你可以这样做 fn.bind(context, firstParam, secondParam, thirdParam) 等等。 - Tim

    5

    setTimeout没有像你期望的那样对index进行快照。所有的超时都会将索引视为最终迭代,因为在超时触发之前,循环已经完成。你可以将其包装在闭包中并传递索引,这意味着闭包中的索引受到对全局index的任何更改的保护。

    (function(index){
        setTimeout(function(){
            self.arr[index] = 'I am John!';
        }, 100);
    })(index);
    

    0

    原因是当settimeout开始时,for循环已经执行完毕,索引值为9,因此所有的计时器基本上都在设置arr[9]。


    -2

    前面的答案是正确的,但提供的源代码有误,其中一个self被错误地打成了elf。解决方案可行。

    另一种方法,不需要使用闭包,在setTimeout语句中只需添加索引参数到函数声明中即可。

    function Demo() {
        this.arr = new Array();
        this.counter = 0;
        this.setMember = function() {
            var self = this;
    
            for(; this.counter < 10; this.counter++){
                var index = this.counter;
                setTimeout(function(){
                    self.arr[index] = 'I am John!';
                }(index), 100);
            }
        };
        this.logMember = function() {
            console.log(this.arr);
        };
    }
    
    var d = new Demo();
    d.setMember();
    
    setTimeout(function(){
        d.logMember();
    }, 1000);
    

    这是不正确的,也无法工作。你将直接调用函数,而不是在100毫秒后调用。你将传递一个参数,但它将被丢弃。然后从外部作用域中获取index,这很好,因为它是同步执行的。然后在100毫秒后什么也不会发生。 - Tim
    如果你将代码剪切粘贴到Firefox的Scratchpad中,它会显示第9个问题“我是约翰”。我只是想帮忙纠正打字错误。 - hgregoire

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接