0
class Simulator {
  constructor() {
    this.gates = Array();
    this.gates.push(new AndGate(200, 200));
  }
  initialize() {
    let canvas = document.getElementById('board');
    canvas.width = 800;
    canvas.height = 500;
    canvas.setAttribute("style", "border: 1px solid black");
    this.gates.push(new AndGate(100, 100));
  }
  run() {
    setTimeout(this.onLoop, 1000);
  }
  onLoop() {
    for (let gate of this.gates) {
      gate.render();
    }
  }
}
let sim = new Simulator();
sim.initialize();
sim.run();

For some reason, the JS transpiled version of my TypeScript class throws an error in the onLoop function. It reports TypeError: this.gates is undefined. However, if I access sim (a Simulator object) and manually access the gates property it's defined. I can run the onLoop code manually from the console.

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
Bast
  • 72
  • 9

3 Answers3

6

When functions are passed by reference, they lose their reference to this. You're losing this reference when calling setTimeout.

Functions have a bind() method that basically return a new function with a corrected reference to this.

Call it as such:

setTimeout(this.onLoop.bind(this), 1000)

Alternatively, you can also pass an in-line arrow function. Arrow functions don't lose their this context.

setTimeout(() => this.onLoop(), 1000)
Evert
  • 93,428
  • 18
  • 118
  • 189
2

When this.onLoop is called within the setTimeout, the calling context inside onLoop is window, because setTimeout is a function of the window object. You can fix that by using an arrow function that calls onLoop, rather than pass onLoop directly:

class Simulator {
  constructor() {
    this.gates = Array();
    //this.gates.push(new AndGate(200, 200));
  }
  initialize() {
    //let canvas = document.getElementById('board');
    //canvas.width = 800;
    //canvas.height = 500;
    //canvas.setAttribute("style", "border: 1px solid black");
    // this.gates.push(new AndGate(100, 100));
    this.gates.push({ render: () => console.log('rendered') });
  }
  run() {
    setTimeout(() => this.onLoop(), 1000);
  }
  onLoop() {
    for (let gate of this.gates) {
      gate.render();
    }
  }
}
let sim = new Simulator();
sim.initialize();
sim.run();

Or by binding the this context of the onLoop function to the instantiated object:

class Simulator {
  constructor() {
    this.gates = Array();
    //this.gates.push(new AndGate(200, 200));
  }
  initialize() {
    //let canvas = document.getElementById('board');
    //canvas.width = 800;
    //canvas.height = 500;
    //canvas.setAttribute("style", "border: 1px solid black");
    // this.gates.push(new AndGate(100, 100));
    this.gates.push({ render: () => console.log('rendered') });
  }
  run() {
    setTimeout(this.onLoop.bind(this), 1000);
  }
  onLoop() {
    for (let gate of this.gates) {
      gate.render();
    }
  }
}
let sim = new Simulator();
sim.initialize();
sim.run();
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
1

Hijacking this question (since it's the first to pop up upon searching "setTimeout this undefined") to post a similar problem I had which suits the title, but not the question's content.

Question

I have this class Foo, and I need to await for a condition which involves members of Foo.

class Foo {
    constructor(bar) { this.bar = bar; }
    
    async async_increment_bar_1(new_bar) {  

        const poll = resolve => {
            if (this.bar === new_bar) {
                resolve();
            } else {
                this.bar += 1;
                setTimeout(_ => poll(resolve), 1000, new_bar);
            }
        };
        
        await new Promise(poll);
    }
}

The first way works just fine, however I find the second one to be more concise, and it also does not pollute the namespace with the poll() function.

class Foo {
    constructor(bar) { this.bar = bar; }

    async async_increment_bar_2(new_bar) {

        await new Promise(function poll(resolve) {
            if (this.bar === new_bar) { // <-- this will raise error
                resolve();
            } else {
                this.bar += 1;
                setTimeout(_ => poll(resolve), 1000, new_bar);
            }
        });
    }
}

However, this raises the following error:

TypeError: Cannot read properties of undefined (reading 'bar')

How can I fix this?

Answer

As per the top answer:

When functions are passed by reference, they lose their reference to this. You're losing this reference when calling setTimeout.

Functions have a bind() method that basically return a new function with a corrected reference to this.

To fix the second code snippet, it's necessary to bind the this reference to the poll() function, at every instance where the poll() function is passed by reference; both at definition and when it's called again, inside the setTimeout lambda.

class Foo {
    constructor(bar) { this.bar = bar; }

    async async_increment_bar_2(new_bar) {

        await new Promise(function poll(resolve) {
            if (this.bar == new_bar) {
                resolve();
            } else {
                this.bar += 1;
                setTimeout(_ => poll.bind(this)(resolve), 1000, new_bar);
            }
        }.bind(this));
    }
}

Full code:

class Foo {
    constructor(bar) { this.bar = bar; }
    
    async async_increment_bar_1(new_bar) {  

        const poll = resolve => {
            if (this.bar === new_bar) {
                resolve();
            } else {
                this.bar += 1;
                setTimeout(_ => poll(resolve), 1000, new_bar);
            }
        };
        
        await new Promise(poll);
    }
    
    async async_increment_bar_2(new_bar) {

        await new Promise(function poll(resolve) {
            if (this.bar == new_bar) {
                resolve();
            } else {
                this.bar += 1;
                setTimeout(_ => poll.bind(this)(resolve), 1000, new_bar);
            }
        }.bind(this));
    }
}

main = async () => {
    
    let foo = new Foo(0);
    
    console.log(`Initially foo.bar is ${foo.bar}`);
    
    await foo.async_increment_bar_2(3);
    
    console.log(`Finally foo.bar is ${foo.bar}`);
}

main();
Alex Mandelias
  • 436
  • 5
  • 10