1

Good day

I recently ran into a strange situation where my this value in on a member function of a class is undefined. I know there are a lot of questions relating undefined this contexts, but I can't find any explanation of this issue. I would love to know why it happens. In this examples, why does the arrow function implementation of the setModified function keep its this context, but the function on the class does not.

This example breaks in the setModified function on the block class

class point {
    constructor(x, y) {
        this._x = x || 0;
        this._changeEvents = [];
    }

    set changeEvent(eventFunction) {
        this._changeEvents.push(eventFunction);
    }

    set x(value) {
        this._x = value;
        this.runChangeEvent();
    }

    set y(value) {
        this._y = value;
        this.runChangeEvent();
    }

    get x() {
        return this._x;
    }

    get y() {
        return this._y;
    }

    runChangeEvent() {
        this._changeEvents.forEach(event => event(this));
    }
}

class renderItem {
    constructor(canvas) {
        this._canvas = canvas;
    }

    render(){

    }
}

class block extends renderItem {
     constructor(canvas) {
        super(canvas);
        this._modified = true;
        this._topLeft = new point(0, 0);
        this._topLeft.changeEvent = this.setModified;
    }

    //Using a method on the class as a callback, it breaks
    setModified(){
         this._modified = true;//breaks, this is undefined
         console.log(this);
     }

    //Sets
    set topLeft(value) { this._topLeft = value; }

    //Gets
    get topLeft() { return this._topLeft }
}
//Creates an instance of the block
const bl = new block(null);
bl.topLeft.x = 20;

But when you change setModified function to an arrow function (also on the class), it works:

class point {
    constructor(x, y) {
        this._x = x || 0;
        this._y = y || 0;
        this._changeEvents = [];
    }

    set changeEvent(eventFunction) {
        this._changeEvents.push(eventFunction);
    }

    set x(value) {
        this._x = value;
        this.runChangeEvent();
    }

    set y(value) {
        this._y = value;
        this.runChangeEvent();
    }

    get x() {
        return this._x;
    }

    get y() {
        return this._y;
    }

    runChangeEvent() {
        this._changeEvents.forEach(event => event(this));
    }
}

class renderItem {
    constructor(canvas) {
        this._canvas = canvas;
    }

    render(){

    }
}

class block extends renderItem {
     constructor(canvas) {
        super(canvas);
        this._modified = true;
        //Using an arrow function on the class instance as a callback, it works
        this.setModified = () => {
            this._modified = true;//works
            console.log(this);
        };
        this._topLeft = new point(0, 0);
        this._topLeft.changeEvent = this.setModified;
    }

    //Sets
    set topLeft(value) { this._topLeft = value; }

    //Gets
    get topLeft() { return this._topLeft }
}
const bl = new block(null);
bl.topLeft.x = 20;

Why would the member function on the class lose the this context but not the arrow function?

blex
  • 24,941
  • 5
  • 39
  • 72
Philip
  • 160
  • 1
  • 8
  • 4
    `this._topLeft.changeEvent = this.setModified;` <= the `setModified` is being transfered to another variable, which does not have the other variable as a property. `this._modified` exists. `this._topLeft._modified` does not – Taplar Jul 03 '20 at 22:28
  • 2
    Note there is a lot of code here not relevant to the specific problem. In future it would help if you scale it down to just a [mcve] and remove anything not relevant – charlietfl Jul 03 '20 at 22:29
  • 4
    Adding to @Taplar's comment, when reassigning, you can call `bind(this)` to prevent losing `this`. `this._topLeft.changeEvent = this.setModified.bind(this);` – Muhammad Talha Akbar Jul 03 '20 at 22:31
  • Ohh... so the scope changes from the block class to the point class, because now it is a member function on the point class. I never thought of it that way. So the function will now have two scopes or closures depending from where it is called from. If I understand you correctly? – Philip Jul 03 '20 at 22:50
  • 1
    I re-created the problem. `this` does not bind to `this._topLeft`. It's actually `undefined`. From the book I reference in my answer, after trying to understand why default binding was occurring: `var bar = obj.foo;` then calling `bar()` Even though `bar` appears to be a reference to `obj.foo`, in fact, it's really just another reference to `foo` itself. Moreover, the call-site is what matters, and the call-site is `bar()`, which is a plain, un-decorated call and thus the default binding applies. I updated my answer, let me know if I said anything wrong. – Diesel Jul 04 '20 at 00:42
  • The above comment was to clarify it doesn't matter if `this._topLeft._modified` did exist, it'd still cause an error. – Diesel Jul 04 '20 at 00:48
  • So, after changing the forEach loop to a for loop in the runChangeEvent function like for (var i =0; i< this._changeEvents.length;i++){ this._changeEvents[i](); } this was available. But this was the instance of the array, which would make sense because the function is now a member of the array. – Philip Jul 04 '20 at 07:55
  • Related: [How to access the correct `this` inside a callback?](https://stackoverflow.com/q/20279484/218196) – Felix Kling Jul 04 '20 at 07:57
  • @Diesel Thanks, you made it clear, very good answer. – Philip Jul 04 '20 at 08:02

1 Answers1

3

An arrow function expression is a syntactically compact alternative to a regular function expression, although without its own bindings to the this, arguments, super, or new.target keywords.

The scope of an arrow function is no different than a regular function, but as you can see above from MDN, they handle the binding of this different.

An arrow function does not create its own binding to this, in fact it inherits from the enclosing scope (which is the class). In the class, the this._modified exists.

An arrow function does not have its own this. The this value of the enclosing lexical scope is used; arrow functions follow the normal variable lookup rules.

this is determined in the execution context, and as we've shown the arrow function works because this stays with the this of the enclosing scope. However with a normal function...

In strict mode, however, if the value of this is not set when entering an execution context, it remains as undefined

If you console.log(this) before the error in your function you will see it is undefined. But why is it undefined? Read here under the implicitly lost section. But basically default binding is applied when calling it as a plain, undecorated function reference, which it is from the assignment. And under strict mode, this will return undefined while not in the global scope.

Implicit binding doesn't occur in your case and this can be implicitly lost.

One of the most common frustrations that this binding creates is when an implicitly bound function loses that binding, which usually means it falls back to the default binding, of either the global object or undefined, depending on strict mode.

If you change your call in runChangeEvent() to event.call(this) you will see that this is actually the point's this (not what you want). If you call this._topLeft.changeEvent = this.setModified.bind(this); you will have the scope you want.

Calling f.bind(someObject) creates a new function with the same body and scope as f, but where this occurs in the original function, in the new function it is permanently bound to the first argument of bind, regardless of how the function is being used.

Check out that link for more information and also I recommend this online book referenced earlier.

Diesel
  • 5,099
  • 7
  • 43
  • 81
  • I am aware that arrow functions inherit their "this". But in this example, both the arrow function and the member function on the class should have access to the same "this"? – Philip Jul 03 '20 at 22:31
  • How are you calling setModified? – Diesel Jul 03 '20 at 22:48
  • https://codesandbox.io/s/sweet-breeze-rxwod?file=/src/index.js I made a code sandbox. Both methods work on here. Which is why I ask. – Diesel Jul 03 '20 at 22:52
  • I get it now. Haha, How did I not think of that. Your code sandbox together with @Taplar comment made it clear.. Thanks – Philip Jul 03 '20 at 22:58
  • 1
    @Philip I got curious about why `this` was undefined for you. I did some reading and tried to clarify my answer. This should help. – Diesel Jul 04 '20 at 00:21