0

As I checked on MDN, method of a class looks like this:

class Foo {
    method1 (){
      // content of method1
    }
}

However I found it's not good for event handlers

<!doctype html>
<html lang="en">
<head>
    <title>test</title>
</head>
<body>
    <div class="settings">
        <div>
            <label for="cb1">checkbox</label>
            <input id="cb1" type="checkbox"></input>
        </div>
    </div>
    
    <script>
        'use strict'
        class TEST{
            box = null;
            info = {content: {blah: "blah"}};
            
            init (){
                this.box = window.document.querySelector(".settings");
                this.box.addEventListener("change", this.handler);
                this.box.addEventListener("change", this.handler2);
            }
            
            handler = e=> {
                console.log("handler this: %o", this);
                console.log("handler info: %o", this.info.content);
            }
            handler2 (e) {
                console.log("handler2 this: %o", this);
                console.log("handler2 info: %o", this.info.content);
            }
        }
        let t = new TEST();
        t.init();
    </script>
</body>
</html>

In the test page above, click the checkbox then the result is result

Reading about arrow function's scope then I understand why ther's the difference. But using arrow function to declare a method of class looks weird, did I do it correctly?

What's more since I don't like there're two kind of function style in one class, I prefer using arrow function for all other methods if possible, but I'm not sure this works for constructor or did it has any potential glitch or secure problem

Any opinion on this please?

Byzod
  • 466
  • 4
  • 18
  • Not sure why you don't like that there's multiple ways to declare a class method. Arrow functions don't rebind `this` which is one reason why they exist. The constructor cannot be an arrow function. That wouldn't make sense. Don't get hung up on "what looks correct" – evolutionxbox Aug 08 '23 at 16:54
  • If you declare the event handler as an ordinary function, `this` will be the event target, not the class instance. – Barmar Aug 08 '23 at 16:55
  • If you don't like it only for the style, you can use this instead: `this.box.addEventListener("change", (e) => { this.handler2(e) });`. It's maybe clearer for the problem, which is that if you pass a function reference to an event handler, it will be executed from the global scope, binded to the HTML element. It's clearer because it's like saying "use a function that preserves this as a handler" – Kaddath Aug 08 '23 at 16:56
  • 1
    Another approach would be to [bind the methods in the constructor](https://stackoverflow.com/questions/38334062/why-do-you-need-to-bind-a-function-in-a-constructor) – evolutionxbox Aug 08 '23 at 16:59
  • Added automatic solution with a class proxy wrapper – Alexander Nenashev Aug 08 '23 at 23:45

2 Answers2

0

handler() is an arrow function, so it inherits this from the outer scope. No worries.

But with methods which are functions in an instance's prototype the situation is different.

When you pass a method as an argument you basically pass it alone without its context (in our case this). There're several fixes how to keep the context:

Use .bind():

this.box.addEventListener("change", this.handler2.bind(this));

Use an arrow function:

this.box.addEventListener("change", e => this.handler2(e));

Bind this in the constructor:

constructor() {
    this.handler2 = this.handler2.bind(this);
}

You can also loop through an object's prototypes in the constructor and bind each method.

But more interestingly is to have some generic solution without modifying classes.

If you want to dive deep into JS proxies and prototypes we can provide a class wrapper to automatically bind all methods in an instance and its prototype chain (it even supports super):

// intercept `new`
const bindThis = what => new Proxy(what, {
    construct(_class, args, constructor) {

        const obj = Reflect.construct(...arguments);

        if (_class.name !== constructor.name) {
            return obj; // the base class, skip it
        }

        const bindContext = _obj => {
            for (const [name, def] of Object.entries(Object.getOwnPropertyDescriptors(_obj))) {
                
                if (typeof def.value === 'function' && name !== 'constructor' && 
                // avoid overridding by base class methods
                !Object.hasOwn(obj, name)) {
                    // bind context for all the methods
                    def.value = def.value.bind(obj);
                    // make look like ordinary props (enumerable)
                    def.enumerable = true; 
                    Object.defineProperty(obj, name, def);
                }
            }
        };

        let context = obj;
        do {
            // skip Object.prototype for clearness
            Object.getPrototypeOf(context) && bindContext(context);
        } while (context = Object.getPrototypeOf(context));

        return obj;
    }
});

const TEST = bindThis(class TEST {
    box = null;
    info = {
        content: {
            blah: "blah"
        }
    };

    init() {
        this.box = window.document.querySelector(".settings");
        this.box.addEventListener("change", this.handler);
        this.box.addEventListener("change", this.handler2);
    }

    handler = e => {
        console.log("handler this: %o", this);
        console.log("handler info: %o", this.info.content);
    }
    handler2(e) {
        console.log("handler2 this: %o", this);
        console.log("handler2 info: %o", this.info.content);
    }
});

const CHILD = bindThis(class CHILD extends TEST {

    isChild = true;
    handler2(e) {
        console.log("OVERRIDDEN");
        super.handler2(e);
    }

});

let t = new TEST();
let c = new CHILD();

t.init();
c.init();
<select class="settings">
<option>-</option>
<option value="1">option 1</option>
</select>
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
0

But using arrow function to declare a method of class looks weird, did I do it correctly?

Yes, this works, but notice they are arrow functions in class fields and not methods.

What's more since I don't like there're two kind of function style in one class, I prefer using arrow function for all other methods if possible, but I'm not sure this works for constructor or did it has any potential glitch?

Yes, you cannot use this style for the constructor, and you should not generally use this because it doesn't work properly with inheritance (cannot be overridden properly, cannot be used with super) and uses more memory than a shared prototype method - the arrow functions are created per instance.

So use this only where you really need it. Alternative approaches are

  • creating the arrow functions explicitly in the constructor, without class field syntax:

    class TEST {
        constructor() {
            this.box = null;
            this.info = {content: {blah: "blah"}};
    
            this.handler = e => {
                console.log("handler this: %o", this);
                console.log("handler info: %o", this.info.content);
            };
        }
        init() {
            this.box = window.document.querySelector(".settings");
            this.box.addEventListener("change", this.handler);
            this.box.addEventListener("change", this.handler2);
        }
        handler2(e) {
            console.log("handler2 this: %o", this);
            console.log("handler2 info: %o", this.info.content);
        }
    }
    
  • defining methods and .bind()ing them explicitly in the constructor:

    class TEST {
        box = null;
        info = {content: {blah: "blah"}};
        constructor() {
            this.handler = this.handler.bind(this);
        }
        init() {
            this.box = window.document.querySelector(".settings");
            this.box.addEventListener("change", this.handler);
            this.box.addEventListener("change", this.handler2);
        }
        handler(e) {
            console.log("handler this: %o", this);
            console.log("handler info: %o", this.info.content);
        }
        handler2(e) {
            console.log("handler2 this: %o", this);
            console.log("handler2 info: %o", this.info.content);
        }
    }
    
Bergi
  • 630,263
  • 148
  • 957
  • 1,375