Javascript : hoisting, scoping and this

Javascript : hoisting, scoping and this

Fourth article in my javascript adventure and this one is a bit special. Each programming language has its particularities and things you should understand before writing code and hitting your head against the wall because of incomprehensible issues. Here is a disjointed overview of javascript's weirdness.

Hoisting

by default, javascript moves all functions and var declarations to the top of your scope. Only declaration is moved, not initialisation.

let x = square(2); // x contains 4

function square(number) {
    return number * number;
}

in reality, the code above was reformated before execution to :

function square(number) {
    return number * number;
}

let x = square(2); // x contains 4

and that'y why it is working.
Now consider the following code:

console.log(a); // prints 'undefined'
console.log(b); // throws an error

var a = 2;
let b = 1;

The reformatted code looks like this:

var a;
console.log(a); 
console.log(b); 

a=2;
let b = 1;

that's why the first call works but prints undefined while the other cannot work since b is declared after the function call.
let and const declarations are never moved to the top.

Scoping

In javascript, you have the global scope:

// b is declared in the global scope and is visible to all elements in this file
let b = 4;

Depending on how you add your scripts in your html file, b is either visible to all scripts:

<script src="/where_b_is.js"></script> 
<script src="/other_js.js"></script> 

or scoped to the file containing it if you're using modules:

<script src="/where_b_is.js"></script> 
<script src="/cant_see_b.js" type="module"></script> 

Each function declares its own scope:

let b = 4;

function test() {
    // b is visible
    // c is only visible in this function
    let c = 1;
}

The general rule is that curly braces {} define new scopes. There are caveats that you should know:

function test(user) {
    let c = 1;
    if(user === 'Max')
    { 
        // This c belongs to the scope of 'if' and does not conflict with the first declaration
        // They both exist, but this one hides the other
        let c = 2;

        // The following will work and will print 'undefined'
        console.log(varAreEvil);
    }
    var varAreEvil = "var are scope to the function, wherever you place them";
}

var must be avoided because of how it behaves compared to let and const.

this

Where What this points to
In an object's method The object
In global scope The global object
In a global function The global object (or undefined with strict mode)
In an event Element that received the event <button onclick="this.style.display='none'">Click</button>

call(), bind() and apply()

Javascript lets you pick and place functions to call them from different contexts.

call() for instance, lets you execute the method of an object within the context of another object as shown below:

const person = {
  fullName: function() {
    return this.firstName + " " + this.lastName;
  }
}
const otherPerson = {
  firstName:"Mike",
  lastName: "Barr"
}

// This will return "Mary Doe"
person.fullName.call(otherPerson); // if the function had parameters, you'd simply put them after 'otherPerson', separated by commas

apply() is almost identical to call() except that any parameter passed to the function should be in an array:

const person = {
  fullName: function(age) {
    return this.firstName + " " + this.lastName + " " + age;
  }
}
const otherPerson = {
  firstName:"Mike",
  lastName: "Barr"
}

// This will return "Mary Doe"
person.fullName.apply(otherPerson, [45]);
// compared to call:
person.fullName.call(otherPerson, 45);

bind() on the other hand, lets you borrow a function from another object.

const person = {
  fullName: function() {
    return this.firstName + " " + this.lastName;
  }
}
const otherPerson = {
  firstName:"Mike",
  lastName: "Barr"
}

// This will return "Mary Doe"
let fullName = person.fullName.bind(otherPerson);
// You can later call fullName();

One of the most common use of bind() is to work around the loss of this in a method, when it is used as a callback.

const person = {
  age: 35,
  do: function() {
    console.log(this.age);
  }
}

setTimeout(person.do, 3000); // will print undefined because 'this' is lost in this use case

the workaround is to do this instead:

let display = person.do.bind(person);
setTimeout(display, 3000);

You can think of functions as lose objects that are attached to a context.
You can refer to these objects from other contexts which hence changes the meaning of this.
In the example above, do is picked as a callback, which means that it will execute in another context where this.age isn't defined.
By binding the function to person and passing this bounded function to the timer, the context of execution is not lose anymore.