Exploring the - inconstant - this in JS

satya - 1/3/2025, 2:30:48 PM

Background

  1. Oh, boy! let me see...
  2. Every function in "js" assumes it has access to an instance of an object, be it global, or a particular object.
  3. So functions often refers to variables using "this.x"
  4. But there are many kinds of function in JS
  5. functions - defined at a global context
  6. methods - functions that belong to a class or an object, or inside other functions
  7. anonymous functions - functions that end up as only variables without names
  8. callback functions - when used to call back from other functions
  9. arrow functions - functions defined inline and simplified in many ways along with their caveats
  10. extracted functions - functions that are passed around by their reference without their parent object, where they start behaving like regular functions and not methods
  11. There are a few factors the literature says, that can effect what the "this" variable points to inside the body of a function. These are:
  12. Where is the function called
  13. Where is the function defined
  14. How is the function called
  15. So.....
  16. Hope to clarify some of these nuances here.
  17. Who is calling the function

satya - 1/3/2025, 2:42:21 PM

The meaning of "how is the function" called....

  1. In literature this phrase is often used "how is the function called" distinct from who is calling it, or where is it being called...
  2. If one is to ignore the special cases of the explicit new, bind, call, etc, there are only 2 ways a function is "classified" to be called.
  3. They are...
  4. Direct call: Call it as just a function, with no reference to the object that is a member of, if it is
  5. Method call: Always called with a prefix of the object.method()
  6. So....
  7. when one says how it is called, they are referring to one of these 2

satya - 1/3/2025, 2:43:00 PM

Examples


//Direct call
f()

//As a method call
o.f()

satya - 1/3/2025, 2:44:14 PM

So all call backs usually falls under the Direct calls


some-function(callback-function) {
    callback-function()
}

satya - 1/3/2025, 2:45:28 PM

Notes

  1. Notice it doesn't matter if the call-back function is a standalone function, or an extracted method name from an object
  2. It also doesn't matter if the calling function is direct, or inside an object, or in a deep hierarchy
  3. It only matters the invocation "reference", the direct method or it is on top of another object.some-method

satya - 1/3/2025, 3:00:29 PM

Here is an illustration of this


// A simple function defined in the global scope
function greet() {
    console.log("Hello, world!");
}

// An object with a method
const person = {
    name: "John",
    greet() {
        console.log(`Hello, my name is ${this.name}`);
    }
};

// Another object with methods for testing callback behavior
const anotherObject = {
    callGreetDirectly() {
        greet(); // Direct call within a method
    },
    callGreetAsCallback(callback) {
        callback(); // Passing and calling the function as a callback
    }
};

// Direct Call
greet(); // Direct Call: Output: Hello, world!

// Method Call
person.greet(); // Method Call: Output: Hello, my name is John

// Calling 'greet' from another object's method
// Direct Call: Output: Hello, world!
anotherObject.callGreetDirectly(); 

// Calling 'greet' as a callback from another object's method
// Direct Call: Output: Hello, world!
anotherObject.callGreetAsCallback(greet); 

// Calling 'person.greet' as a callback (context is lost)
// Direct Call: 
anotherObject.callGreetAsCallback(person.greet); 

//Output: Undefined behavior or 
//"Hello, my name is undefined" because 'this' is lost

satya - 1/3/2025, 3:05:09 PM

The behavior of anonymous functions is similar to regular functions when "this" is concerned

  1. They have similar regarding the variability or invariability of the "this" variable
  2. Of course, the arrow functions behave differently.. for they are also kind of anonymous, you can say, but have distinct semantics for "this"

satya - 1/3/2025, 3:10:01 PM

Because callbacks loose the "this" parameter

  1. When a called calls a callback, which is essentially a "function", there is no object reference at that point,
  2. So the method of invocation becomes "direct" loosing the "this"
  3. So design the callbacks so that they rely as much as possible on what is passed in
  4. Of course "closures" can help some...thats a different topic
  5. Also know that arrow functions behave a bit differently, yet, you want to minimize the dependency and expect them to behave like stateless functions.

satya - 1/4/2025, 10:03:55 PM

Now to arrow functions....a key design element, necessary and good, read on

Now to arrow functions....a key design element, necessary and good, read on

satya - 1/4/2025, 10:09:56 PM

Few assertions first on the arrow functions

  1. The "this" of an arrow function is fixed. It cannot be changed unlike most other types of functions, methods, etc.
  2. In other words, when an arrow function is invoked, it carries with it, where ever it goes, the original this where it is defined.
  3. To put this in common words, if an arrow function is defined inside an object's method as a callback, that callback method carries with it the "this" pointer of its parent object!

satya - 1/4/2025, 10:15:41 PM

Lets start with a harmless example of a method as the target of a call back


const obj = {
  name: "Sathya",
  greet() {
    console.log(`Hello, ${this.name}!`); // `this` refers to `obj` if called directly
  },
  delayedGreet() {
    setTimeout(this.greet, 1000); // Passing the method as a callback
  },
};

obj.delayedGreet(); // What happens to `this` in `greet`?

satya - 1/4/2025, 10:20:42 PM

Commentary

  1. Idea is we send an object method into a callback
  2. Being an object method, it expects to use the instance properties of that object
  3. But if you notice....
  4. Being a callback, the method, being now an extracted method as far as the caller is concerned, looses the "this"
  5. So that method when called back can not use any instance variables!! ouch!
  6. There are some strange syntactic work arounds to this but we won't go into that here...

satya - 1/4/2025, 10:24:00 PM

You can fix this by explicitly telling JS to remember it by using bind


const obj = {
  name: "Sathya",
  greet() {
    console.log(`Hello, ${this.name}!`);
  },

  delayedGreet() {
    setTimeout(this.greet.bind(this), 1000); 
    // Explicitly bind `this` to `obj`
  },

};

obj.delayedGreet(); // Output (after 1 second): "Hello, Sathya!"

satya - 1/4/2025, 10:28:45 PM

Here is how to fix this using an arrow function


const obj = {
  name: "Sathya",
  greet() {
    console.log(`Hello, ${this.name}!`);
  },
  delayedGreet() {
    setTimeout(() => this.greet(), 1000);
    // Arrow function inherits `this` from `delayedGreet`
  },
};

obj.delayedGreet(); // Output (after 1 second): "Hello, Sathya!"

satya - 1/4/2025, 10:31:00 PM

So, an arrow function call back is like a call back on the object itself

  1. When a regular function is called back, it looses its parent object (unless a closure is used, which is a different topic)
  2. where as when an arrow function from an object is used as a callback, it is as if the object itself is tagged along, and participates in the callback

satya - 1/4/2025, 10:37:45 PM

By extension you can think of the arrow function like a closure on the object where it is defined

  1. In JS a function is a bit strange
  2. Unlike in other languages when a function is passed around, it carries with it the local sister variables to that function, in other words, its neighborhood
  3. this is what is called a closure
  4. However a function DOES NOT CARRY the Object instance along with it by default
  5. Where as when that function is an arrow function, it DOES, allowing a much more natural experience

satya - 1/4/2025, 10:38:20 PM

Given so many ways to retain this in callbacks, what are the key design patterns for JS Callbacks?

Given so many ways to retain this in callbacks, what are the key design patterns for JS Callbacks?

Search for: Given so many ways to retain this in callbacks, what are the key design patterns for JS Callbacks?

satya - 1/4/2025, 10:58:16 PM

Few more notes on arrow functions

  1. They borrow their this from their parent contexts
  2. Their this cannot be altered
  3. When their parent is an object, they carry their object around
  4. They also carry their parent functions argument list (not to be confused with the explicit arguments that are passed into them in their definition)
  5. You cannot change their this via call or bind
  6. You can use these properties to pass the whole object to respond to a call back

satya - 1/4/2025, 10:59:03 PM

What are the most frequently used approaches for callbacks in JS?

What are the most frequently used approaches for callbacks in JS?

Search for: What are the most frequently used approaches for callbacks in JS?

satya - 1/4/2025, 11:04:12 PM

They are

  1. Arrow functions
  2. Methods with explicit binding with .bind[often used]
  3. Use static methods
  4. Use closures
  5. inline anonymous functions when reference to this is not needed and can use just the input params

satya - 1/4/2025, 11:04:42 PM

Why Use an Inline Anonymous Function Instead of an Arrow Function?

Why Use an Inline Anonymous Function Instead of an Arrow Function?

Search for: Why Use an Inline Anonymous Function Instead of an Arrow Function?

satya - 1/4/2025, 11:08:52 PM

When

  1. In cases like event listeners where the caller like the button wants to pass their own this to the function,... you cannot do that if that is an arrow function
  2. When you need to use the "arguments" object
  3. when you want to force a method to use a particular object as its this

satya - 1/4/2025, 11:09:13 PM

See this curios example


function logName() {
  console.log(this.name);
}

const person = { name: "Sathya" };

[1, 2, 3].forEach(logName.bind(person)); // Binds `this` to `person`

satya - 1/6/2025, 4:35:58 PM

Can I call new on a stand alone function even if it doesn't use the "this" variable?

  1. Say I have a (sf) stand alone function outside of an object and in the main line
  2. Say further it may or may not refer to this in its body
  3. ...
  4. What happens if I call new on that function?
  5. Yes you can call new on that sf
  6. As the function does not use the "this" that gets passed in
  7. The resulting this at the end of the function call is an empty object {}

satya - 1/6/2025, 4:38:09 PM

So what makes a function a "cf"

  1. Every "sf" can be called as if it is an "sf"
  2. Usually a "cf" wants to define some "structure" or body to the passed in "this"
  3. This structure include attributes and functions
  4. General indication is that if an "sf" is referencing a "this", it is likely meant to be used as a cf

satya - 1/6/2025, 4:52:46 PM

Are sub functions always members of an object instance?

  1. Usually cfs or classes define sub functions, other functions that are properties of an object
  2. These are often called methods
  3. ....
  4. Can a mainline non-cf define sub functions?
  5. What does it mean to do so?
  6. Who is the parent to such sub functions if there is no object to hold them?
  7. Whose attributes are they?
  8. ....
  9. The answer is this
  10. Like a function that has local variables like integers and strings
  11. It can also have a baby function inside with inputs and outputs, including visibility and access to the parent functions local variables
  12. ....
  13. So the baby child function is just a variable in the local scope
  14. It can be called by other baby functions inside
  15. It cannot be accessed by anything outside the parent sf unlike when they are from a cf and part of an object
  16. ....
  17. Another nuance of JS is that these child functions follow closure rules
  18. So when you return one of these local functions out, they carry the context of the parent! This is also often called closure
  19. So you CAN access the internal function if and only if, it is returned by the parent function as a response

satya - 1/7/2025, 5:36:13 PM

The three common lexical scopes in JS

  1. block - like the body of a loop, or an if, {}, let, const etc.
  2. function - inside the body of a function, variables, params, inner functions
  3. global - outside of a function, variables, objects, functions at a global level

satya - 1/7/2025, 5:43:00 PM

Difference between lexical scope and lexical context

  1. One is static and one is run time
  2. The lexical scope is the most obvious referring to what is inside {}, give and take
  3. The lexical context is the "run time environment" for a piece of code to run and what variables are available at run time for that code to refer to
  4. Unspecified run time variables like this, arguments, thread level variables etc all form the context

satya - 1/7/2025, 5:47:02 PM

Why am I going to great lengths to understand these 2 names: scope and context?

  1. Many rules around how "this" is determined at run time for a variety of functions are described using terms scope and context
  2. So to understand the documentation you have to know how these two differ
  3. So...know that there are 3 scopes: block, function, and global. Roughly that's it. Which are explained above and familiar to most programming languages.
  4. Whereas the lexical context, the run time arguments like this, or more accurately context, varies with in the same scope depending on if it is an arrow function or a non-arrow function

satya - 1/7/2025, 5:49:33 PM

On top of that... the object literal throws a wrench into the context of an arrow function due to scope rules

  1. An object literal defined at a global level, although uses {} to scope its code, that scope inherits the scope from its parent and does not explicitly create a new run time context (this, arguments etc)
  2. This is much like the {} for the for loops, if, etc, where there is no new context created but uses the context from its parent scope

satya - 1/7/2025, 5:52:53 PM

This is why an arrow function in an object literal behaves differently than one in a cf

  1. An arrow function inside a cf, uses the "scope" of the cf, and that scope creates a new context and new this, which is used by the arrow function, and never changes once it is created
  2. Where as an arrow function in an object literal, the scope is the object literal, but the object literal's scope doesn't create a new execution context (this) but borrows from the scope above it, the global, making the this null.
  3. In other words some scopes create a new run time context and some don't

satya - 1/7/2025, 5:54:09 PM

The following scopes create a new runtime context

  1. function scopes - always (except the arrow)
  2. global scopes - some times (when they are not in strict mode

satya - 1/7/2025, 5:55:11 PM

The following scope(s) DOES NOT create a new context but inherits the context of its parent

  1. Block quotes like,
  2. for
  3. if
  4. object literal
  5. etc.

satya - 1/7/2025, 6:35:35 PM

That, in short solves the mystery of the arrow function behavior in an object literal vs cf

  1. The scope of cf creates a new context (this).
  2. That gives the af (arrow function) inside the cf its this
  3. ....
  4. The scope of an object literal (ol) is more like a block scope and merely uses its parents run time context
  5. So for an af in an object literal, the "this" is the global "this" which is there sometime and some time not
  6. .....
  7. However, if there is a regular function rf() inside the ol, and that rf() creates and passes an af inside itself, then the scope and context is that of the rf() which is the "this" of the rf() and all works ok.

satya - 1/7/2025, 6:35:53 PM

Consider this


ol = {
    rf() {
          someobj.callback(() => this.x)
    }
}

1. is the scope of the arrow function (af) here inside rf() same as rf?
2. And hence the context of the af is the context of rf?

satya - 1/7/2025, 7:08:26 PM

Commentary

  1. The scope of af is rf as it is defined inside the rf
  2. Moreover, the rf scope being an executable scope it gets its own context, and that context (this) is what gets fixed to the af as it travels
  3. ....
  4. The rf itself is inside ol
  5. ol is like a variable in the global scope
  6. ....
  7. Apparently ol does not create a new lexical scope
  8. variables outside the ol are accessible inside the ol as they are in the same lexical scope

satya - 1/7/2025, 7:09:51 PM

Consider this


ol = {
    rf() {
          //call this callback af1()
          someobj.callback(() => this.x)
    }
    af2() = () => this.y
}

satya - 1/7/2025, 7:12:03 PM

Specially the af2()

  1. It is an arrow function
  2. Rule says: it inherits the "this" from its parent lexical scope
  3. ...
  4. Unlike a function, an object literal ol, does not create its own lexical scope
  5. so, the lexical scope of af2 is the same as the global scope
  6. ...
  7. so af2 is tied to the "this" of the global scope

satya - 1/7/2025, 7:31:56 PM

Scope, lexical scope, and context

  1. Oh, boy, back to these nuances!
  2. ...
  3. lexical scope
  4. They say, lexical scope is the static scope
  5. It does walk up the chain
  6. ...
  7. blocks create a new lexical scope with let and const but not var
  8. object literals DO NOT create a new lexical scope (important)
  9. ...
  10. scope
  11. This is considered dynamic
  12. Also walks up the chain
  13. Depending on the objects passed in this can change, distinguishing from the lexical scope
  14. ....
  15. context
  16. The run time objects that are available in that scope

satya - 1/7/2025, 7:32:49 PM

A quick note on the closure

  1. It remembers all the following up the chain...
  2. ...
  3. Variables declared in the same block.
  4. Variables declared in the enclosing function scope.
  5. Variables declared in any higher scopes, including the global scope.

satya - 1/7/2025, 7:34:02 PM

Back to this code


ol = {
    rf() {
          //call this callback af1()
          someobj.callback(() => this.x)
    }
    af2() = () => this.y
}

satya - 1/7/2025, 7:36:26 PM

The af2()

  1. The rule says, the "this" of the "parent lexical context
  2. ...
  3. The parent of af2 is "ol"
  4. ol does not create its own lexical context, so it is the same as the global
  5. ...
  6. so the lexical context of af2() is global
  7. So it is the global "this" and not the "ol" this

satya - 1/7/2025, 8:21:20 PM

The oddness of Object literals and their lexical scope

  1. Of inside and outside of an ol, is the same lexical scope, question arises how come outside cannot see the internal variables of the ol
  2. ....
  3. The suggested answer is, they are NOT variables inside, the WHOLE OBJECT is the variable, and the inner variables are merely the properties of that object and hence getting around the visibility of them being separate variables.
  4. ...
  5. nuance, nuance...