我认为我在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()调用步骤时会丢失实例信息?
我怎么能重构这个,所以我可以通过超时循环并仍然可以访问这些实例变量?
答案 0 :(得分:6)
当您致电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);
}
答案 1 :(得分:1)
当您调用Javascript函数时,this
的值由调用网站确定。
当您将this.step
传递给setTimeout
时,this
并未神奇地保留;它只是传递了step
函数本身
setTimeout
将this
的回调称为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);
}