skip to Main Content

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?

2

Answers


  1. 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 achieve that:

    Use .bind():

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

    Use an arrow function:

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

    THere could be some automatic context binding like the below. Also there’re some problems with inheritance as well as in Bergi’s example (we are overriding prototypes). So that’s why binding on demand like my first 2 proposals could be preferable.

    So if you want to dive deep into JS proxies and prototypes:

    // intercept `new`
    const bindThis = what => new Proxy(what, {
        construct(_class, args) {
            const obj = new what(...args);
            const bindContext = _obj => {
                for (const [name, def] of Object.entries(Object.getOwnPropertyDescriptors(_obj))) {
                    if (typeof def.value === 'function' && name !== 'constructor') {
                        // bind context for all the methods
                        def.value = def.value.bind(obj);
                        Object.defineProperty(_obj, name, def);
                    }
                }
            };
    
            let context = obj;
            do {
                bindContext(context);
            } while ((context = Object.getPrototypeOf(context)) && Object.getPrototypeOf(context));
    
            return obj;
        }
    });
    
    const TEST = bindThis(class {
        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();
    <select class="settings">
    <option>-</option>
    <option value="1">option 1</option>
    </select>
    Login or Signup to reply.
  2. 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);
          }
      }
      
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search