开发者问题收集

使用 setInterval 调用的方法报告 Javascript 对象属性未定义

2019-05-15
122

我正在尝试理解 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,而是调用全局函数吗?

3个回答

在 JavaScript 中, this 的规则有些复杂;相关的规则是,如果将存储在对象属性中的非箭头函数作为方法调用,则可以将 this 分配给对象。让我们来解析一下:

  • game.updategame 对象的属性,✅
  • 它包含一个非箭头函数,✅
  • 它作为方法调用... ❌

“作为方法调用”是什么意思?这意味着您在 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);};
    
Amadan
2019-05-15

当您使用以下语法定义内部函数时: 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>
ganjim
2019-05-15

由于 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);

此处 了解有关 箭头函数 this 的更多信息

Ali Elkhateeb
2019-05-15