JavaScript Theory (2/2)

Yesterday I made a post with as many concepts of theoretical JS I could think of that I have learned throughout all the YouTube videos, articles read, and courses taken and, it turned out to be quite long, which made me split it into two parts. So here is part 2.

Hoisting

Remember that a variable has an environment, a scope and the this keyword.

Hoisting is how variables are created in JS. It makes some variables usable in the code before they are actually declared in the code.

Before the execution, the code is scanned for variable declarations, and for each variable, a new property is created in the variable environment object. This happens during the creation phase of the Execution context. For each variable found in the code, a new property is created in the variable environment object.

Hoisting works differently depending on the type of variable:

  • function declarations: They are hoisted. Their initial value is the function. Their scope is block-scoped. This means we can use function declarations before they are actually declared on the code.
  • var Variables: They are hoisted. Their initial value is undefined. Their scope is function-scoped. When we try to access a var variable before its declared in the code, we get an undefined value.
  • let and const variables: They are not hoisted in practice, but in theory they are. Their initial value is uninitialized (TDZ temporal dead zone). Their scope is block-scoped. If we attempt to use a let or const variable before its declared we get an error. Let and Const are block scoped, so they exist only in the block they were created.
  • function expressions and arrow functions: If they were created using var, they are hoisted to undefined; if they were created using let or const, they will not be in the TDZ and will be usable until we declare them.

Taking Hoisting and the TDZ to practice

We'll jump in directly into coding and I'll leave notes after each code part ok? Ok.

console.log(firstName); // 1
console.log(userName); // 2
console.log(age); // 3 

var firstName = "Eric";
let userName = "Anomadsoul";
const age = 32


1- undefined
2- ReferenceError: Cannot access "userName" before initialization
3- ReferenceError: access "age" before initialization

I called the functions before declaring them so, the result of each line of code is after the //... this happens thanks to Hoisting.

The variables userName and age are temporarily in the TDZ, and will leave that zone until they are declared, only then we can call them.

For functions, take the code below:

console.log(addDeclaration(1, 1)); // 4
console.log(addExpressions(1, 1)); // 5
console.log(addArrow(1, 1)); // 6

function addDeclaration(a, b){ // 7
    return a + b;
};

const addExpressions = function (a, b) { // 8
    return a + b;
};

var addArrow = (a, b) => a + b; // 9


4- This will print "5"
5- This will print "ReferenceError"
6- This will print "addArrow is not a function"
7- This is a function expression
8- This is a function expression
9- This is an arrow function

The expression will print a "ReferenceError" because functions stored in const variables will be in the TDZ until they are declared.
The arrow function will print "Is not a function" because in the console.log we are trying to call an undefined variable (because it hasn't been defined yet)

Pitfall of Hoisting

Take the code next to understand the concept:

if(!numUsers) deleteUsers();

var numUsers = 10;

function deleteUsers() {
    console.log(`Users deleted`);
}


Output will be: Users deleted

Even though we have 10 users in the variable numUsers. This happens because we are calling the variable before defining it, and since the value returned from this variable that is yet to be defined, is undefined, and since undefined is a falsy value just like 0 or false, then the variable is set to 0 for that particular case function, thus deleting the Users. This happens because of Hoisting.

Due to this, the best practices are:
- Do not use var unless it is extremely necessary
- Declare the variables at the top of each scope
- Always declare the functions first and use them after the declaration

The This Keyword

The this keyword or this variable, is created for every execution context, and therefore, any function.

The this keyword will always take the value of the owner of the function in which the keyword is used. It is also said that is points to the owner of that function.

The value of the this keyword is not static, it depends on how the function is called and it is only assigned when the function is called.

Let's take calling functions as an example:

  • Using a method:
const user = {
    name: "Eric",
    birthYear: 1988,
    calcAge: function() {
        return 2021 - this.birthYear;   // 10
        // 11
    }
};
console.log(user.calcAge());            // 12


10- The value of .this is user, because it is the object that it is calling.
11- Calling it as user.year would be another way of doing it, but then we wouldn't be using the .this keyword.
12- We use a method to call the function (.calcAge)

  • Calling them as normal functions (not attached to objects) only works in strict mode.

  • Using arrow Functions (which are not a way to call functions) doesn't work because arrow functions do not get the this keyword in their execution context.

  • Called as an event listener: The this keyword will always point to the DOM element that the handler function is attached to.

this will never point to the function that we are using it in, and it will never point to the variable environment of the function.

Let's take the following code to clarify:

const user = {
    name: "Eric",
    birthYear: 1988,
    calcAge: function() {
        return 2021 - this.birthYear;
    }
};
console.log(user.calcAge());

const user2 = {
    birthYear: 2016,
};

user2.calcAge = user.calcAge;           // 13
console.log(user2.calcAge());

Ouput: 33 5

13- This is called method borrowing

Because the this keyword is not calling user but it is calling the object that calls the function, that is why when we use user and user2 to call the function, the this keyword calls each object.

Regular functions vs Arrow functions

Take the next nested functions, an arrow function inside a regular function:

const user = {
    firstName: "Eric",
    birthYear: 1988,
    calcAge: function() {
        return 2021 - this.birthYear;
    },

    greet: () => console.log(`Hey ${this.firstName}`),
};

user.greet();

This code won't work properly, because arrow functions do not have its own this keyword property, but it would actually work if it was a regular funcion inside a regular function. Since the arrow function doesn't have its own this, it uses its parent's this, making the object of the keyword, the global scope, not the funtion scope.

The result will be: "Hey undefined" because the arrow function doesn't have access to the parent function variables through the this keyword.

This happens when we try to access a property that doesn't exist on a certain object we get an undefined result.

This behaviour happens oftenly when we use var as variables keyword instead of const or let. When we use the keyword var, it created a property in the global object, and if we call the global object through the keyword this, we will actually be calling that keyword, but it is a bad practice, it not well written code.

As a best practice, we should never use the arrow function as a method, even if we are not using the this keyword in said function.

To make the code above work we'd have to do as follows:

const user = {
    firstName: "Eric",
    birthYear: 1988,
    calcAge: function() {
        return 2021 - this.birthYear;
    },

    greet: function () {
        console.log(`Hey ${this.firstName}`)
    }
};

const hola = function(){
    console.log("Hola");
}

user.greet();
hola();

Output:
Hey Eric
Hola

Note to self: The this keyword doesn't have access to parent or sibling functions if we use it inside functions that already have a this keyword, meaning that this cannot call this that in turn calls an object, we would have to store the *this keyword in a variable outside of the function:

For a more clear example:

const user = {
    firstName: "Eric",
    birthYear: 1988,
    calcAge: function () {
        return 2021 - this.birthYear;

    const isMillenial = function () { 
        console.log(this.year >= 1981 && this.year <= 1996);
    };
    isMillenial();                 // 14
    },

    greet: () => {
        console.log(this);
        console.log(`Hey ${this.firstName}`);       // 15

    },
};

user.greet();
user.calcAge();

14- This is a regular function call even though it happens inside of a method.
15- Arrow functions will result in undefined using the this keyword.

This code won't run properly because we are calling the isMillenial function that already has a this keyword, and in turn the this keyword is calling the object user. We have to store the this keyword in a variable that usually is called self.

const user = {
    firstName: "Eric",
    birthYear: 1988,
    calcAge: function () {
        console.log(2021 - this.birthYear);

        const self = this;          // 16
        const isMillenial = function () { 
            console.log(self.birthYear >= 1981 && self.birthYear <= 1996);    // 17
    };
    isMillenial();      
    },

    greet: function () {
        console.log(`Hey ${this.firstName}`);      // 18

    },
};

user.greet();
user.calcAge();

16- We set the self function to the function and now we have access to it outside of it. We could also use that instead of *self.
17- We use self inside instead of this
18- We change the arrow function for a regular function

/The that solution is old, and there are actually more modern ways to solve this issue, and we can actually do that with an Arrow function.

Remember that Arrow functions do not get their own this keyword but instead use the this from it's parent's scope? Well, that used in our favor here:

Basically, an arrow function inherits the this keyword from its parents.

const user = {
    firstName: "Eric",
    birthYear: 1988,
    calcAge: function () {
        console.log(2021 - this.birthYear);

        const isMillenial = () => { 
            console.log(this.birthYear >= 1981 && this.birthYear <= 1996);    
    };
    isMillenial();      
    },

    greet: function () {
        console.log(`Hey ${this.firstName}`);    
    },
};

user.greet();
user.calcAge();

Just like the This keyword, functions also get access to an arguments keyword. It is only available in regular functions.

const addExpr = function (a, b) {
    console.log(arguments);
    return a + b;
}

addExpr(2 , 5);

Output:
[Arguments] { '0': 2, '1': 5 }

We can use this when we need a function to accept more parameters than we actually specified:

console.log(addExpr(2, 5, 8));

Output:
[Arguments] { '0': 2, '1': 5, '2': 8 } 7

The arguments keyword is not that useful in modern JS, but it is good to know it exists.

Primitives vs objects

There's a lot of confusion between primitives and objects, and it mainly comes from cases like the one I'm going to show you:

let age = 32;           // 18
let oldAge = age;       // 19
age = 33;               // 

console.log(age);       // Output: 33
console.log(oldAge);    // Output: 32

18- We set the value of the variable
19- This one stays the same even though we set the value as the same, when we change the value of age, oldAge doesn't change.
20- We change the value of said variable

But now let's check this scenario where Eric has a friend named Eric:

const eric = {
    name: "Eric",
    age: 32
};
const friend = eric;
friend.age = 27         // 21

console.log("Friend", friend);  // Output: Friend { name: 'Eric', age: 27 }
console.log("Eric", eric);      // Output: Eric { name: 'Eric', age: 27 }

21- We change the value of the variable age inside of the object friend.

As you can see, the output of eric is wrong, Eric is 32, not 27 yrs old.

Since we changed the age of the friend, we are also changing the age of Eric, because in objects Eric = friend.

Now you can see the confusion, but the difference lies in where the types are stored:

  • Primitives data types (Primitive types):
    Everything that is a Number String Boolean Undefined, Null Symbol and BigInt. They are stored in the call stack (in the execution context in which they are declared).

  • Objects (Reference types):
    Everything else that is not a primitive, such as Object literal, Arrays, Functions and everything else. They get stored in the memory heap. The heap is an almost unlimited memory pool.

Since we are setting to be the same both Friend and Eric, they both point to the same object in the memory Heap (so, if we change that object value, we change it for both objects), whereas when we set two variables as equal, they point to different Addresses and values in the call stack (so, if we change the value of the variable, we are just changing the value of the specific address to which the variable is pointing, leaving the other address as is).

Even though we set the variable friend as a constant, we can still manipulate the properties and values inside the object, because we are not changing the value in the call stack, but in the Heap.
The principle of immutability of const is only true for primitives.

Whenever you think you are copying an object, in reality you are just creating a new object that points to the same piece in the Heap as the one that you equalized it to.

Let's put these concepts into practice:

For primitives:

let dogName = "Butch";
let oldName = dogName;
dogName = "Pixie";
console.log(`I was ${oldName}, but now I'm ${dogName}`);        // Output: I was Butch, but now I'm Pixie

Now for objects:

let woman = {
    firstName: "Mary";
    lastName: "Williams";
    age: 27;
};

And let's say Mary got married

let marriedWoman = woman;       // 22

22- We are pointing to the same object on the stack, if we change anything from the object in the code, it will change both objects.

We can't change the property values of neither object without affecting both objects (because in the end, they point to the same object in the memory Heap).

But we can actually copy an object instead of making it point to the same spot of the memory heap, so we can change one of them without affecting the other? We use the object.assign function, check it out:

let woman = {
    firstName: "Mary",
    lastName: "Williams",
    age: 27
};

We will merge two objects and return a new one:

const marriedWoman = Object.assign({}, woman);       // 25- 

Now we can actually change the new object without affecting the one we copied.

marriedWoman.lastName = "Federer";

console.log(`Before marrying I was ${woman.lastName} but now I am ${marriedWoman.lastName}`);

25- We use an empty object and merge it with the object we want to copy.

Object assign only works on the first level, meaning that if we have a nested object, the inner object will still point to the same memory heap as the first one, so it is a shallow copy, not a deep clone. If we manipulate an array inside the copied object, the array will change in both objects.

It is doable, but outside my paygrade to create deep clones, but perhaps in a later post once I'm well versed in the topic.



For now, that is it, you've hit my knowledge level of JS, at least in theory. I hope you thoroughly read throughout this thorough post, I will see you in the next topic.

H2
H3
H4
3 columns
2 columns
1 column
Join the conversation now
Ecency