2011-09-06 31 views
3

我想我错过了一些关于JavaScript中对象和原型函数的关键概念。为什么通过setTimeout调用原型函数时会丢失实例信息?

我有以下:

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

我然后创建蹦床的一个实例,并调用它的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”未定义。所以,在我的step函数中,第二个调用会爆炸说没有未定义的“length”属性。

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

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

回答

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

击败了我20秒,并以一个整洁的例子来启动。做得好。 – Beejamin

1

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

当您通过this.stepsetTimeout时,this不会神奇地保留;它只是通过step函数本身。
setTimeout呼叫其回拨this作为window

你需要创建一个合适的对象上调用step封闭:

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

有关this和封锁,see my blog之间的区别的详细信息。

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

这是相当标准的 '本' 范围的问题。在执行功能时,关于错误理解“this”的上下文的许多许多问题。我建议你阅读它。

但是,回答您的问题,它的工作原理是因为您呼叫这个。step()和'this',就是你想要的Bouncer实例。

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

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

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

好,清楚的解释。 – Beejamin

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); 
} 
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如果它不存在添加到所有的功能。

相关问题