我想我错过了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);
}