Is there a way to enable a modern "this" scope in a JS module and remove the need for the "this." prefix?

161 Views Asked by At

I have the following module class and to make it work similar to non module functional javascript, I have to call a function to bind all the class member functions to the correct this scope and I have to explicitly add this. when referencing class members.

Is there a way to remove these requirements so that module code looks exactly the same as functional code (see how it works in typescript in nodejs).

The only thing to pay attention to in the code examples is the addition of this. and the requirement to call bindProperties() function in the module class. If you don't call the bindProperties() function the variables and the classes and the event handlers all lose scope. The this in those cases could be anything. The content of the example code is not important.

Including javascript in an HTML page (notice no mention of this in any of the code or in the event handler):

<script src="myscript.js"></script>

// myscript.js
"use strict"
var button = document.getElementById("button");
button.addEventListener("click", buttonClicked); 

function buttonClicked(event) {
    alert("Hello world");
}

The same thing in a module class is the following (notice the multiple uses of this. and the necessary call to bind() in the constructor):

<script src="myscript.js" type="module"></script>

// myscript.js
export App class {
    button = null;

    constructor() {

      try {
         this.bindProperties(App);
         this.button = document.getElementById("button");
         this.button.addEventListener("click", this.buttonClicked);
      }
      catch(error) {
         this.log(error);
      }
   }

   buttonClicked(event) {
      alert("Hello world");
   }

   // this method is required to make the `this.` point to the class in all of the functions and event handlers
   bindProperties(mainClass) {
      var properties = Object.getOwnPropertyNames(mainClass.prototype);
      for (var key in properties) {
         var property = properties[key]
         if (property!=="constructor") {
            this[property] = this[property].bind(this);
         }
      }
   }
}

What I'd like to know is if there is a setting to remove the need for needing to write this. and to remove the need to call, bindProperties(). Note the lack of this. and the lack of bindProperties():

<script src="myscript.js" type="module" version="2"></script>

// myscript.js
export App class {
    button = null;

    constructor() {

      try {
         button = document.getElementById("button");
         button.addEventListener("click", buttonClicked);
      }
      catch(error) {
         console.log(error);
      }
   }

   buttonClicked(event) {
      alert("Hello world");
   }
}

Basically, is there an option to remove the need for adding this and an option to remove the need for calling bindProperties.

What am I talking about? Look at typescript/javascript in nodejs. You never need to use this., you don't need to call binding function on classes. The scope is always what you expect it to be.

But for client side javascript, I'm using typescript it is aware of the scope issue and by default it adds this. to any class members with code complete. And it flags it if it doesn't have it.

This is confusing for Javascript newbies who might not understand why they need to bind the this on class modules when they don't have to do it in non module scripts or in nodejs.

But maybe I'm missing something. I hope this makes sense.

Again, if you write code in a normal script included in a web page, you don't need to do any of these scope things that you have to do when using a module.

In other words, is there a setting to make modules code and behavior syntactically the same as non modules / nodejs code? The answer is probably no obviously but typescript has so many options maybe I missed it.

Update
In this linked post is this comment:

Changing myVariable to this.myVariable fixes the issue, but doing that for every variable clutters up my code quite a bit.

And once he starts building modules he will have the other issues I mention above.

3

There are 3 best solutions below

7
Peter Seliger On

There are actually just two technical approaches, which both lead to the desired behavior.

First, an anonymous arrow function expression as handler makes use of retaining the this value of the enclosing lexical context, hence the App instantiation time's this.

// `App` module scope

// a single function statement for handling any `App` instance's
// button-click, based on event and app instance references which
// both get forwarded by an anonymous arrow function expression
// which retains the `this` value of the enclosing lexical context
// at `App` instantiation time.

function handleAppButtonClick(appInstance, evt) {
  const computedValue = 2 * appInstance.publicProperty;

  console.log({
    computedValue, appInstance, button: evt.currentTarget,
  });
}

/* export */class App {

  #privateButton;
  publicProperty;

  constructor(buttonSelector = '#button', value = 10) {
    value = parseInt(value, 10);

    this.publicProperty = isFinite(value) ? value : 10;

    this.#privateButton =
      document.querySelector(buttonSelector);

    this.#privateButton.addEventListener(

      // - an arrow function as handler makes use of retaining
      //   the `this` value of the enclosing lexical context,
      //   hence the `App` instantiation time's `this`.

      "click", evt => handleAppButtonClick(this, evt)
    );
  }
  // no need for any prototypal implemented event handler.
}
// end ... App module scope


// other module's scope
// import App from '...'

new App('button:first-child');
new App('button:nth-child(2)', 20);
.as-console-wrapper { max-height: 82%!important; }
<button type="button">1st test logs ... 20 ... as computed value</button>
<button type="button">2nd test logs ... 40 ... as computed value</button>

Second, a single function statement for handling any App instance's button-click. It is the base for creating handler-functions which explicitly do bind an App instance as the created handler's this context.

// `App` module scope

// a single function statement for handling any App-instance's
// button-click. It is the base for creating handler-functions
// which explicitly do bind an `App` instance as the created
// handler's `this` context.

function handleButtonClickOfBoundAppInstance(evt) {
  const computedValue = 2 * this.publicProperty;

  console.log({
    computedValue, appInstance: this, button: evt.currentTarget,
  });
}

/* export */class App {

  #privateButton;
  publicProperty;

  constructor(buttonSelector = '#button', value = 10) {
    value = parseInt(value, 10);

    this.publicProperty = isFinite(value) ? value : 10;

    this.#privateButton =
      document.querySelector(buttonSelector);

    this.#privateButton.addEventListener(

      // - explicitly bind an `App` instance as
      //   the created handler's `this` context.

      "click", handleButtonClickOfBoundAppInstance.bind(this)
    );
  }
  // no need for any prototypal implemented event handler.
}
// end ... App module scope


// other module's scope
// import App from '...'

new App('button:first-child');
new App('button:nth-child(2)', 20);
.as-console-wrapper { max-height: 82%!important; }
<button type="button">1st test logs ... 20 ... as computed value</button>
<button type="button">2nd test logs ... 40 ... as computed value</button>

3
Bergi On

Is there a way to remove these requirements so that module code looks exactly the same as functional code?

Yes - don't use a class. Use a function for functional code:

function myscript() {
    const button = document.getElementById("button");
    button.addEventListener("click", buttonClicked); 
    
    function buttonClicked(event) {
        alert("Hello world");
    }
}

or put the code in an ES module:

<script src="myscript.mjs"></script>

// myscript.mjs
const button = document.getElementById("button");
button.addEventListener("click", buttonClicked); 

function buttonClicked(event) {
    alert("Hello world");
}

If you are going to define a class App and instantiate it, and put properties on the instance(s), then you will have to refer to those properties using dot syntax. There is no way around this.

1
Amadan On

I do not know which languages you are familiar with. In Python, if you have a class, instantiate it and refer to a method, like this:

class Dog:
    def __init__(self, name):
        self.name = name

    def bark():
        print(f"{self.name} says woof!")

fido = Dog("Fido")
print(fido.bark)
# => <bound method Dog.bark of <__main__.Dog object at 0x1102ff690>>

you can see that fido.bark is a "bound method". It means that it knows what its receiver is:

fido_bark = fido.bark
fido_bark()
# => Fido says woof!

Even if we take the property of fido.bark and assign it to something else, it still knows it will be executing in context where self is fido.

(In fact, Python makes it pretty explicit, in that fido.bark() is equivalent to Dog.bark(fido). In effect, fido.bark has fido bound as its first parameter, that will be passed to self parameter that has to be explicitly defined in Python.)

JavaScript does not create bound methods. It just has functions. Let's write this in JavaScript:

class Dog {
  constructor(name) {
    this.name = name
  }
  bark() {
    console.log(`${this.name} says woof`)
  }
}

const fido = new Dog("Fido")
fido.bark()
// => Fido says woof

console.log(fido.bark)
// => bark() { console.log(`${this.name} says woof`) }

const fido_bark = fido.bark
fido_bark()
// weirdness ensues

Why does weirdness ensue? Because in JavaScript, this is assigned at method call time. JavaScript needs the dot syntax in order to know what this should be inside the function. In fact, the above code is basically a new syntactic sugar for this, more or less:

const Dog = function(name) {
  this.name = name
}

const _bark = function bark() {
  console.log(`${this.name} says woof!`)
}

Dog.prototype.bark = _bark

const fido = new Dog("Fido")
fido.bark()
// => Fido says woof!

console.log(fido.bark)
// => function bark() { console.log(`${this.name} says woof!`) }

const fido_bark = fido.bark
fido_bark()
// weirdness ensues

The crucial thing is that bark knows that this is Fido only if we call it like fido.bark(). If you read the value of fido.bark, it is just a function. In fact, it is just _bark (which is also found in fido_bark), which obviously have no clue which dog it applies to — or that it is even related to dogs! It is the fido. in the call syntax that makes it bind fido as the receiver (to be assigned to this). When you just invoke fido_bark(), this does not get this special assignment. If you have a function, you can create a bound function like this instead:

const real_fido_bark = fido_bark.bind(fido)

Now real_fido_bark already has the this assigned to fido, even if you don't invoke it with the dot syntax:

real_fido_bark()
// Fido says woof!

This distinction between bound methods and unbound methods exists in other OOP languages as well. E.g. in Ruby, with the difference that all methods start off as bound:

class Dog
  def initialize(name)
    @name = name
  end

  def woof
    puts "#@name says woof!"
  end
end

fido = Dog.new("Fido")
fido_woof = fido.method(:woof)
puts fido_woof
# => #<Method: Dog#woof() a.rb:6>
fido_woof.call
# => Fido says woof!

unbound_woof = fido_woof.unbind
puts unbound_woof
# => #<UnboundMethod: Dog#woof() a.rb:6>
unbound_woof.bind_call(fido)
# => Fido says woof!

real_fido_woof = unbound_woof.bind(fido)
real_fido_woof.call
# => Fido says woof!

See also What is the difference between a function, an unbound method and a bound method? for C#.

So when you pass this.buttonClicked into .addEventListener, this is equivalent to

const handler = this.buttonClicked
button.addEventListener("click", handler)

This means you pass an unbound function into addEventListener, and it will have no idea what this should be. To counteract this, you have to either make a bound function explicitly:

const handler = this.buttonClicked.bind(this)

or to make the event invoke it using the dot syntax:

const handler = (evt) => {
  this.buttonClicked(evt)
}

Here, handler itself is an unbound function, but since it is an arrow function it closes over this of its outer scope, which is exactly the this we want — and then invokes the method correctly.


As for your other question, in JavaScript, bare identifiers always refer to either local or global variables, never properties. Properties can be accessed using the dot syntax (fido.name) or the square bracket syntax (fido["name"]); name is never a reference to an object property. There is no way around this, this is just how JavaScript works. Some other languages allow you to leave out the object reference, making it a bit ambiguous what name would refer to if you see it in isolation. E.g. in C# and Ruby, if you see name, you can't be sure if it is a field of the current object (C#) or a method call (Ruby), or maybe just a variable, without inspecting the surrounding code further. In JavaScript, there is no such confusion, as property access has its own distinct syntax. There is no way around it, and you should not look for one.

If you do not like JavaScript, you can write not-JavaScript. Many languages transpile to JavaScript these days. Of course, this will complicate your development, and make your code less readable to other frontend developers who do not use the same technology, so I do not recommend this path. CoffeeScript used to be a pretty popular choice, though its use has declined a lot. These days, TypeScript is the only widely popular language that transpiles to JavaScript, and it keeps the syntax as close to JavaScript as possible; you will still have to write this. for property access, it will not eliminate those pesky five characters for you.


EDIT: Now, nothing here has anything to do with modules. It also has nothing to do with scope, except in the second handler example, where scope determines what value this is inside the arrow function. It only has to do with the difference between bound and unbound functions, and the method invocation syntax vs function invocation syntax.