使用 setInterval 调用的方法报告 Javascript 对象属性未定义
我正在尝试理解 Javascript,并希望制作一个简单的 Web 小程序,可以通过按钮启用和禁用,并在启用时绘制内容。为了编写干净的代码,我想为此使用一个对象。带有按钮的页面的设置是
<div>
<input type="button" name="runbutton" value="Run" onclick="game.run();"/>
<input type="button" name="resetbutton" value="Reset" onclick="game.reset();"/>
</div>
<script>
//...
</script>
,而 javascript 代码是
function Game() {
this.runs = false;
this.run = function() {console.log('run...'); this.runs = true;};
this.reset = function() {console.log('reset...'); this.runs = false;};
this.update = function() {console.log('updating... runs:', this.runs);};
};
var game = new Game();
game.reset();
setInterval(game.update, 300);
因此,它是一个对象定义 (Game),带有一个实例 (game),该实例具有一个布尔属性 (runs) 和三个方法。一个运行它,一个停止运行,以及一个报告它是否运行的 update() 方法。使用 setInterval 每 300 毫秒重复一次 update()。
问题 :来自 update() 的控制台日志将 this.runs 的值报告为未定义,而不是 false 或 true。当我打开控制台并暂停它以检查变量时,它会正确地将 game.runs 报告为 false 或 true。此外,当我在设置 this.runs 之前和之后向 run() 和 reset() 添加 console.log() 调用以报告其值时,它似乎正确地报告了 true 和 false。因此问题似乎出在 update() 的某个地方。好像它使用了错误的“this”。也许 setInterval 不能用于方法?
我尝试了代码的另外两种语法,但它们似乎有完全相同的问题:
var game = {
runs: false,
run: function() {console.log('run...'); this.runs = true;},
reset: function() {console.log('reset...'); this.runs = false;},
update: function() {console.log('update... runs:', this.runs);}
};
game.reset();
setInterval(game.update, 300);
以及在对象内设置 setInterval 的版本:
var game = {
runs: false,
i: undefined,
run: function() {console.log('run...'); this.runs = true; this.i = setInterval(this.update, 300);},
reset: function() {console.log('reset...'); this.runs = false; clearInterval(this.i);},
update: function() {console.log('update... runs:', this.runs);}
};
game.reset();
同样的问题。
发生了什么?为什么 update() 将 this.runs 报告为未定义?我是否正确地认为方法中的“this”在所有情况下都确实指的是游戏实例?我不应该在方法上使用 setInterval,而是调用全局函数吗?
在 JavaScript 中,
this
的规则有些复杂;相关的规则是,如果将存储在对象属性中的非箭头函数作为方法调用,则可以将
this
分配给对象。让我们来解析一下:
-
game.update
是game
对象的属性,✅ - 它包含一个非箭头函数,✅
- 它作为方法调用... ❌
“作为方法调用”是什么意思?这意味着您在
object.property
语法上调用一个函数,如下所示:
game.update(...)
。
但是,
game.update
作为参数传递,它失去了与
game
的连接。您的代码相当于:
var func = game.update;
setInterval(func, 300);
其中
setTimeout
将仅调用
func()
。这意味着
game.update
被作为函数而不是方法调用,并且
this
在调用时不会设置为
game
。
典型的解决方法是:
-
将接收器绑定到函数。这是除上述方法调用之外的另一种设置
this
的方法:如果函数绑定到接收器对象,则在调用时它将始终将this
设置为该对象。您可以这样写:setInterval(game.update.bind(game), 300)
React 中经常使用的一种变体是在定义位置将函数明确绑定到接收器:
this.update = function() {console.log('updating... runs:', this.runs);}; this.update = this.update.bind(this);
-
通过以下任一方式明确使用方法调用:
setInterval(() => game.update(), 300); setInterval(function() { game.update(); }, 300);
-
使用箭头函数以词汇方式定义
this
。由于this
是定义函数时的游戏对象,因此将它们变成箭头函数会始终将this
设置为该游戏对象。这需要在定义时进行更改,而不是在调用时进行更改:this.update = () => {console.log('updating... runs:', this.runs);};
当您使用以下语法定义内部函数时:
function() {
则此函数将具有其自己的
this
,因此
this.runs
将未定义,如果您希望 this 成为父函数的对象,则有两个选项:
选项 1: 将内部函数定义为箭头函数:
function Game() {
this.runs = false;
this.run = () => {console.log('run...'); this.runs = true;};
this.reset = () => {console.log('reset...'); this.runs = false;};
this.update = () => {console.log('updating... runs:', this.runs);};
};
var game = new Game();
game.reset();
setInterval(game.update, 300);
<div>
<input type="button" name="runbutton" value="Run" onclick="game.run();"/>
<input type="button" name="resetbutton" value="Reset" onclick="game.reset();"/>
</div>
选项 2:
将父函数
this
存储为变量
function Game() {
self = this
this.runs = false;
this.run = function() {console.log('run...'); self.runs = true;};
this.reset = function() {console.log('reset...'); self.runs = false;};
this.update = function() {console.log('updating... runs:', self.runs);};
};
var game = new Game();
game.reset();
setInterval(game.update, 300);
<div>
<input type="button" name="runbutton" value="Run" onclick="game.run();"/>
<input type="button" name="resetbutton" value="Reset" onclick="game.reset();"/>
</div>
由于
this
的上下文不再是
game
,因此
game.update
被调用作为
setInterval
的回调,如果您使用箭头函数,它将修复该问题。
function Game() {
self = this
this.runs = false;
this.run = function() {console.log('run...'); self.runs = true;};
this.reset = function() {console.log('reset...'); self.runs = false;};
// Using arrow function () => {} instead of normal function
this.update = () => {console.log('updating... runs:', self.runs);};
};
var game = new Game();
game.reset();
setInterval(game.update, 300);