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

3

-3

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.

Arenicolous answered 25/12, 2023 at 4:50 Comment(21)
What would globalVariable be in the second example? If it is supposed to be classVariable, then this is not possible, as classVariable cannot be accessed directly in that manner.Wayzgoose
...that is, not without some hacks, such as with: function C() {}/C.prototype.foo = 42;/C.prototype.bar = function() { with (this) { console.log(foo); } }/new C().bar() // 42. with statements are forbidden in strict context (which ES6 classes enforce implicitly), so unless you are willing to use ES5 functions to do OOP...Wayzgoose
This sounds like an XY problem. If such a class is only meant to be a namespace and the methods are all static, perhaps you don't need a class, but an IIFE that returns a namespace object (e.g. Math).Wayzgoose
What do you mean by "module class"? You shouldn't use classes for modularisation. Use ES modules, or plain functions.Dramatic
Does this help? https://mcmap.net/q/304178/-javascript-do-i-need-to-put-this-var-for-every-variable-in-an-object/…Dramatic
@Dramatic yes, similar question. in your link is this comment, "Changing the bar to this.bar fixes the issue, but doing that for every variable clutters up my code quite a bit." yes that's part of the issue. and in modules I need to explicitly call a function like bindProperties to get the behavior I expect. This is necessary in nodejs using typescript but in js modules using typescript it is. I'm looking to get the nodejs syntax / behavior / scope in js modules.Arenicolous
@Dramatic it seems that maybe there might be a setting in typescript - to "fix scope issues in modules", but it seems like it might be in the existing features of javascript to have module class code look the same as nodejs module class code. I've updated the post moreArenicolous
@1.21gigawatts ... "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." This is a bold statement which I do challenge. There is no difference of how one writes code that deals with a correct this context, regardless of the environment ... node.js versus browser/client side, module encapsulated code versus functions declared within a global scope ... it all does not make any difference. Please provide example code which proves me wrong.Aretino
@1.21gigawatts ... "If you don't call the bindProperties() function the variables and the classes and the event handlers all lose scope." ... Nope, nothing looses scope except methods which are used as unbound functions, like what happens with the prototypal buttonClicked method which gets passed as an event-listener's callback-handler. In the very moment of being invoked by the internal listener's handleEvent method, the this context of cause is different than the one of e.g. appInstance.buttonClicked(). Thus one either does bind explicitly or via an arrow function's lexical scope.Aretino
@1.21gigawatts I'm not sure, are you confusing JS modules (type="module", import & export) with classes? Those are two separate things, you don't need to combine them. Which one do you want to use, and why? It's not even quite clear what you think is wrong with the original "non module functional javascript", it seems that is the most simple and straightforward code. The particular example you've shown does not benefit from anything that modules or classes have to offer imo.Dramatic
@PeterSeliger ""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." you're right. let me rephrase. if you write everything in a single flat structure and don't nest function or if you use arrow functions you should generally avoid those scope issues. but you're right. what i'm talking about is i don't want to explicitly have to add this. to every class variable. and i don't want to have to bind this on all event handler calls. other languages handle this somehowArenicolous
what i'm talking about is, if it's possible, i don't want to explicitly have to add this. to every class member and i don't want to have to bind this on all event handler calls. other languages handle this somehow. there might be a name for it. there might be a name for how javascript has different scopes compared to other languages. maybe it's different bc js is prototype based. but i'm asking if there is a name for this scoping behavior and if there is an option to change it. if not that's fine.Arenicolous
@1.21gigawatts ... 1/3 ... o.k. I think we need to clarify terminology. A module is something entirely different then a class. A module has scope. Think of it in terms of a closure. Regardless of the environment, be it node or client side, the code implemented within a module does not differ. A constructor function (hence a class or a function used as constructor) works with this in order to refer to the currently bound context. In addition, at instantiation/construction time, a constructor function might create enclosed scope as well. But scope and context are entirely different things ...Aretino
@1.21gigawatts ... 2/3 ... I still do not know what you're exactly trying to achieve. But constructors are only needed in order to create more than just one instance of a specific type. Classes or constructors can be implemented within the global scope or within a module scope where they get exported from. Modules can be written in every JS environment they are supported either natively or can be recreated by immediately invoked function expressions which return a value.Aretino
@1.21gigawatts ... 3/3 ... In order to get the best possible advise, tell us what exactly you want to achieve; from a modeling perspective while using the correct meaning of scope and context.Aretino
JavaScript is not other languages. If you are writing JavaScript, just write JavaScript. To access a property of a class, you need the object and a dot (or square brackets). To access a local variable. you don't. Your buttonClicked is a property, so you have to access it like a property. Or you can go write e.g. Opal Ruby instead and compile Ruby to JavaScript, so you don't ever need to write this. To clean this up, instead of this.buttonClicked, use evt => this.buttonClicked(evt) or this.buttonClicked.bind(this), and then you don't need ugly hacks like bindProperties.Binns
@Binns that can be answer. "there is no setting do this. look at opal ruby to avoid this".Arenicolous
@1.21gigawatts "i don't want to explicitly have to add this. to every class member and i don't want to have to bind this on all event handler calls. other languages handle this somehow" - no. Very few languages mess up the distinction between scope variables and object properties/attributes. And for autobinding, see #41128019 and #20279984. But yes, JavaScript is not Ruby!Dramatic
@Dramatic Not quite on topic, and it is a nitpick, but even Ruby distinguishes object attributes (using the @ glyph). It does not distinguish variables from method invocations.Binns
@Binns Ah, thanks, I don't know any Ruby :-)Dramatic
It seems that maybe I DO have to use this. in a node based typescript class...Arenicolous
B
3

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.

Binns answered 26/12, 2023 at 1:32 Comment(1)
Thank you. You spared me from writing a similar extensive answer at the 2nd holiday.Aretino
D
2

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.

Dramatic answered 25/12, 2023 at 21:57 Comment(3)
A lot to unpack here... but yes, it would work. Is there a name for declaring all the variables right on the code block like you've done (the variables are not defined inside of a class/or function)? Because doing it the way you've shown with modules and exports, you can sort of make an object / class out of it when you do the import. See import * as myObjectClassThing from "myscript.mjs";Arenicolous
@1.21gigawatts They're called "top-level declarations" or "module-scoped variables". And yes, you can export them, but you haven't shown an example where you would need that.Dramatic
ok i've exported them before but i refrained to keep the focus of the postArenicolous
A
0

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>
Aretino answered 25/12, 2023 at 14:54 Comment(7)
Thanks. I used the arrow function handler before to solve this. I used that for a while until I converted to using javascript modules and then I ran into the issues mentioned. I've updated the post with more details.Arenicolous
What is going on with #privateButton. You can use a # in the name of a variable?!?Arenicolous
The '#' prefix marks a property as true private field which can only be accessed within the constructor function and within prototype methods.Aretino
@1.21gigawatts ... "If you don't call the bindProperties() function the variables and the classes and the event handlers all lose scope." ... Nope, nothing looses scope except methods which are used as unbound functions, like what happens with the prototypal buttonClicked method which gets passed as an event-listener's callback-handler. In the very moment of being invoked by the internal listener's handleEvent method, the this context of cause is different than the one of e.g. appInstance.buttonClicked(). Thus one either does bind explicitly or via an arrow function's lexical scope.Aretino
@petersiliger regarding losing scope: there might be something to do with typescript on this but i know 100% for sure the event handlers lose the class scope if declared as members of a class. using the arrow function with a lamda function keeps the scope but that's not how i define event handlers. if i wrote this same code in C# or Java the event handler scope would point to the class, correct? so is there a name for how java and c# do it versus how javascript is doing it? is there language feature or runtime option to say, "when calling this event handler, use the scope of the function"Arenicolous
Siliger regarding the # prefix: how come I never heard of this before now? how come I never saw this until your example?Arenicolous
@PeterSiliger regarding the # prefix: how come I never heard of this before now? this is a rhetorical question i never saw this until your example. looks like it's been supported since 2021...Arenicolous

© 2022 - 2024 — McMap. All rights reserved.