Javascript Contexts

Javascript Contexts

One of the most popular programming languages since early 2000(if not the one), Javascript is considered an object-oriented language. For those which whom this term is unfamiliar, it means in Javascript we can naturally apply some of the core concepts of object-oriented programming, which uses the declaration of classes and objects and use their properties and methods(the functions within the object).

Let's observe the vehicle class as an example:

class Vehicle {
    name;
    numberOfPassengers;
    move;

    constructor(name, numberOfPassengers, move){
        this.name = name;
        this.numberOfPassengers = numberOfPassengers;
        this.move = move;
    }
}

We see that a vehicle has a few variables defined: name, numberOfPassengers, and move. By default, all classes have a constructor method, that serves to build a new object of the given class. The constructor method of our Vehicle class receives parameters that are assigned to its variables.

Note that in order to assign the parameters to the Vehicle variables, the constructor uses the word this. before the name of the variables. The this keyword refers to the context surrounding that function, which encompasses everything inside the curly brackets of the class definition.

The way we build a vehicle is by using the new keyword with a call to the class name, assigning it to a variable like in the sample below

function drive(){
    console.log(“driving a ” + this.name)
}
var car = new Vehicle(“Ferrari”, 2, drive)

console.log(car.name)
console.log(car.numberOfPassengers)
console.log(car.move())

\\ Expected output:
\\ Ferrari
\\ 2
\\ driving a Ferrari

We can see that our new Vehicle is a Ferrari that can take up to 2 passengers. It also receives a function, drive that prints to the console. We can access our car properties and log into the console and, we can call its move method and the result is also printed to the console.

However, let's consider the ride function below. It is very similar to the drive function from the previous snippet.

function ride(){
    console.log(“riding a ” + this.name)
}
ride()

\\ Expected output:
\\ riding a undefined

If we call the ride function as it is, we get a riding a undefined message in the console. That happens because when we call it there is no name variable in that context to be referenced. When we build an object and pass it a function that uses the surrounding context the function automatically detects and uses it. That is how our plane object below works fine just, like our previous car Vehicle.

function fly(){
    console.log(“piloting a ” + this.name)
}

var plane = new Vehicle(“Boeing 777-300”, 368, fly)
plane.move()

\\ Expected output:
\\ piloting a Boeing 777-300

That’s a very simple way to understand how contexts work in Javascript, but at times we get into more complex situations. For instance, let's say you want to watch for changes in a file and show the content of this file every time it changes. So you’ll use the watch function from the node's fs module. Let’s say your fileHandler class is similar to the one below:

import { watch, promises } from "fs";

class FileHandler {
    watch(event, filename) {
        this.showContent(filename);
    }

    async showContent(filename) {
        // the content comes as a buffer, so we convert it to string
        const content = await promises.readFile(filename).toString()
        console.log(content);
    }
} 

const fileHandler = new FileHandler() 
watch(__filename, fileHandler.watch);

If we try to pass our fileHandler.watch method like in the snippet above, it will be ignored by the language, that happens due to another concept of object-oriented programing naturally applied in Javascript, which is inheritance(a class can inherit properties and methods from another class, enabling code reuse and hierarchical relationships between classes).

Instead, the language will try to use the watch method of the FSWatcher object, within the fs module, which has an internal watch method. However, this object does not have a showContent method like our custom fileHandler, a situation that will trigger a TypeError.

This is a context problem that can be solved by specifying in which context the function we are trying to call is. For that we have three methods inherited by all Javascript objects: bind, call, and apply.

Bind

The bind method creates a copy of a function bound to the context provided as the first argument. If we wanted to solve our problem with this method, we could do it the way demonstrated below

watch(__filename, fileHandler.watch.bind(fileHandler));

It might look redundant at first, but this way we make sure that during execution, the watch method we are calling will be the one contained in the fileHandler object. As the bind method creates a copy, we can also assign the bound method to a new variable, like this:

var boundFileHandlerWatch = fileHandler.watch.bind(fileHandler)
watch(__filename, boundFileHandlerWatch);

Call

The call method, calls the function changing the context to the context passed as the first argument. Solving our problem with that method would look like this:

watch(__filename, fileHandler.watch.call(fileHandler));

Apply

The apply method, similarly to the previous call method, calls the function changing the context to the context passed as the first argument.

The only difference here is how we would pass arguments for the method we are changing context. While the call method takes the arguments individually, the apply method takes an array of values as a second argument.

As an example, observe our fileHandler. Its watch method receives two arguments: the event, and the filename:

class FileHandler {
    watch(event, filename) {
        this.showContent(filename);
    }

    async showContent(filename) {
        const content = await promises.readFile(filename).toString()
        console.log(content);
    }
}

To mimic the way fs.watch calls our fileHandler.watch method we could change the context of our method to another context where there is a mock showContent method and pass it the arguments like in the samples below:

var callContext = { showContent: () => console.log(“hello call!”)} 
fileHandler.watch.call(callContext, "change", __filename)

var applyContext = { showContent: () => console.log(“hello apply!”)}
fileHandler.watch.apply(applyContext, ["change", __filename])

\\ Expected output:
\\ hello call!
\\ hello apply!

We see both call and apply are very similar. Whenever you use them just be careful to pass the arguments in the right order, because the parameters will be received respectively.

That’s all, folks! Next time you pass methods between contexts make sure you specify them correctly so that you won’t run into such problems.