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



我想我错过了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来循环step函数。

这在第一次调用step()时有效。然而,在第二次调用时(setTimeout触发步骤时),实例变量"balls"是未定义的。因此,在我的阶跃函数中,第二次调用将显示未定义的没有"length"属性。

为什么我失去了我的实例信息时,从setTimeout()调用步骤?

我该如何重组这个,这样我就可以通过超时循环,仍然可以访问这些实例变量?

当您调用setTimeout(this.step, 1000);时,step方法失去了其所需的this上下文,因为您正在传递对step方法的引用。在你现在这样做的时候,当this.step通过setTimeout, this === 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);
}

调用Javascript函数时,this的值由调用站点决定。

当您将this.step传递给setTimeout时,this不会神奇地保留;它只传递step函数本身。
setTimeout调用this作为window的回调函数。

你需要创建一个闭包,在正确的对象上调用step:

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

有关this和闭包的更多信息,请参阅我的博客

这是相当标准的' This '作用域问题。在执行函数时,关于SO的许多问题都误解了"this"的上下文。我建议你仔细研究一下。

然而,回答你的问题,它工作是因为你是调用 this.step(),而'this',在那个上下文中,是你想要的Bouncer实例。

第二次(以及随后的)工作,因为当你指定一个函数由setTimeout调用时,它是由'window'上下文调用的。这是因为您正在传递对阶跃函数的引用,而该引用中没有包含上下文。

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

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

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

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

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


如果需要支持不支持.bind()的JS环境,我提供的文档链接给出了一个解决方案,在大多数情况下是足够的。

From the docs:

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;
    };
}

这将通过Function.prototype添加.bind() shim到所有函数,如果它还不存在的话。

我很确定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);
}

这是一个由@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);
}