为什么在通过setTimeout调用原型函数时会丢失实例信息?

时间:2011-09-06 15:40:40

标签: javascript

我认为我在JavaScript中缺少关于对象和原型函数的一些关键概念。

我有以下内容:

function Bouncer(ctx, numBalls) {
    this.ctx = ctx;
    this.numBalls = numBalls;
    this.balls = undefined;
}

Bouncer.prototype.init = function() {
    var randBalls = [];
    for(var i = 0; i < this.numBalls; i++) {
        var x = Math.floor(Math.random()*400+1);
        var y = Math.floor(Math.random()*400+1);
        var r = Math.floor(Math.random()*10+5);
        randBalls.push(new Ball(x, y, 15, "#FF0000"));
    }
    this.balls = randBalls;
    this.step();
}

Bouncer.prototype.render = function() { 
    this.ctx.clearRect(0, 0, 400, 400);  
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].render(this.ctx);
    }
}

Bouncer.prototype.step = function() {
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    this.render();
    setTimeout(this.step, 1000);
}
然后我创建了一个Bouncer实例并像这样调用它的init函数:

$(function() {
    var ctx = $('#canvas')[0].getContext('2d');
    var width = $('#canvas').width();
    var height = $('#canvas').height();



    var bouncer = new Bouncer(ctx, 30);
    bouncer.init();
});

init()函数将调用具有setTimeout的步骤来循环步进函数。

这适用于第一次调用step()。但是,在第二次调用时(当setTimeout触发步骤时),实例变量“balls”未定义。因此,在我的步骤函数中,第二个调用会爆炸,说明未定义的“长度”属性。

为什么在从setTimeout()调用步骤时会丢失实例信息?

我怎么能重构这个,所以我可以通过超时循环并仍然可以访问这些实例变量?

6 个答案:

答案 0 :(得分:6)

当您致电setTimeout(this.step, 1000);时,step方法会丢失其所需的this上下文,因为您正在传递对step方法的引用。正如您现在所做的那样,当this.step通过setTimeoutthis === window而不是您的Bouncer实例进行调用时。

这很容易解决;只需使用匿名函数,并保留对this

的引用
Bouncer.prototype.step = function() {
    var that = this; // keep a reference
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    this.render();
    setTimeout(function () { 
        that.step()
    }, 1000);
}

答案 1 :(得分:1)

当您调用Javascript函数时,this的值由调用网站确定。

当您将this.step传递给setTimeout时,this并未神奇地保留;它只是传递了step函数本身 setTimeoutthis的回调称为window

您需要创建一个在右侧对象上调用step的闭包:

var me = this;
setTimeout(function() { me.step(); }, 500);

有关this和闭包之间差异的更多信息,see my blog

答案 2 :(得分:1)

这是相当标准的“这个”范围问题。关于在执行函数时错误理解“this”的上下文,有很多关于SO的问题。我建议你仔细阅读。

但是,要回答你的问题,它的作用是因为你正在调用 this.step(),而在这种情况下,'this'是你想要的Bouncer实例。

的第二次(以及后续)次,因为当你指定一个由setTimeout调用的函数时,它会被'window'上下文调用。这是因为您正在传递对step函数的引用,并且该引用中不包含上下文。

相反,您可以通过从匿名方法内部的正确范围调用来维护上下文:

var self = this;
setTimeout(function(){ self.step(); }, 1000);

答案 3 :(得分:1)

我很确定setTimeout执行的任何操作都发生在全局范围内,因此对this的引用不再指向您的函数,它指向window

要修复它,只需在步骤中将this缓存为局部变量,然后在setTimeout调用中引用该变量:

Bouncer.prototype.step = function() {
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    this.render();
    var stepCache = this;
    setTimeout(function () { stepCache.step() }, 1000);
}

答案 4 :(得分:1)

其他人指出调用上下文问题,但这是一个不同的解决方案:

setTimeout( this.step.bind( this ), 1000 );

这使用the ECMAScript 5 bind()[docs]方法发送一个函数,其调用上下文绑定到您传递的任何内容作为第一个参数。


如果需要支持不支持.bind()的JS环境,我提供的文档链接提供的解决方案足以满足大多数情况。

来自文档:

if (!Function.prototype.bind) {
    Function.prototype.bind = function (oThis) {
        if (typeof this !== "function") // closest thing possible to the ECMAScript 5 internal IsCallable function
        throw new TypeError("Function.prototype.bind - what is trying to be fBound is not callable");
        var aArgs = Array.prototype.slice.call(arguments, 1),
            fToBind = this,
            fNOP = function () {},
            fBound = function () {
                return fToBind.apply(this instanceof fNOP ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)));
            };
        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();
        return fBound;
    };
}

如果.bind()垫片尚未存在,则会通过Function.prototype将{{1}}垫片添加到所有功能中。

答案 5 :(得分:0)

这是一个关闭问题,如@SLaks所示。

试试这个:

Bouncer.prototype.step = function() {
    for(var i = 0; i < this.balls.length; i++) {
        this.balls[i].yPos -= 1;
    }      
    var self = this;
    this.render();
    setTimeout(function() {self.step();}, 1000);
}