2011-09-06 53 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如果它不存在添加到所有的功能。

相關問題