Unpacking the Trickiest Concepts in JavaScript
Tags: javascript, beginners, tutorial, webdev
Published: 2023-02-18 15:00:25

JavaScript is a powerful and versatile language that has become a cornerstone of web development. However, as developers build more complex and dynamic applications, they often encounter tricky concepts that can be challenging to master. From scopes and closures to prototypes and type coercion, JavaScript presents a range of challenges that can trip up even experienced developers.
In this article, we'll unpack some of the trickiest concepts in JavaScript and provide tips and strategies for understanding and working with them. Whether you're a beginner looking to build a strong foundation or an experienced developer seeking to deepen your knowledge, this guide will help you tackle some of the most complex and confusing aspects of JavaScript. So let's dive in and unravel the mysteries of JavaScript together.



## Closures 

Closures are a powerful concept in JavaScript that allows a function to access variables from its parent function's scope, even after the parent function has returned. Closures are created whenever a function is created and the inner function retains access to its parent function's scope, including any variables or functions declared within it. Closures are created when a function is defined inside another function and has access to the outer function's variables, which are "closed over" and retained by the closure.
Closures are often used to create private variables and functions in JavaScript. By enclosing a variable or function inside another function, we can prevent other parts of the program from accessing or modifying it directly. This can help to ensure data privacy and prevent naming conflicts.
Here's an example of how closures can be used to create a private variable in JavaScript:

```
    const outerFunction = () => {
      const outerVar = 'I am in the outer function!';
    
      const innerFunction = () => {
        console.log(outerVar);
      }
    
      return innerFunction;
    }
    
    const finalFunc = outerFunction();
    finalFunc(); // Output: "I am in the outer function!"
    
```

In this example, `outerFunction` defines a variable `outerVar` and a function `innerFunction`, which references `outerVar`. When `outerFunction` is called, it returns `innerFunction`, which is then assigned to the variable `finalFunc`.
When `finalFunc` is called, it logs the value of `outerVar` to the console, even though `outerVar` is declared in the parent function `outerFunction`. This is because `innerFunction` retains a reference to its parent function's scope, and can access `outerVar` even after `outerFunction` has returned.
Closures are particularly useful for creating private variables and functions in JavaScript, as they can be used to encapsulate data and functionality within a function's scope. They are also used extensively in functional programming, where higher-order functions that return other functions are common.
However, closures can also cause memory leaks if they are not used carefully, as they can keep references to variables and functions in memory even when they are no longer needed. It's important to understand how closures work and how to use them effectively to avoid potential issues.

## Scopes

In JavaScript, scope refers to the visibility and accessibility of variables and functions within a program. Every function in JavaScript creates its own scope, which determines the lifetime and visibility of the variables and functions declared inside the function. The scope of a function can be divided into two parts: local scope and global scope.

### Local Scopes

Local scope is created every time a function is called and is destroyed when the function returns. Any variables and functions declared inside the function are only visible within that function and its nested functions. Local scope is also known as function scope

```
    const add = (a, b) => {
      const result = a + b;
      return result;
    }
    
    console.log(add(2, 3)); // logs 5
    console.log(result); // Uncaught ReferenceError: result is not defined

```   

In this example, the `add` function creates a local variable `result` that is only accessible inside the function. When `add` is called with the arguments `2` and `3`, it returns the result `5`. However, when we try to log the value of `result` outside of the function, we get a `ReferenceError` because `result` is not visible outside the function ( global scope).

### Global Scope
Global scope, on the other hand, is created when the program starts and is accessible from anywhere in the program. Variables and functions declared in the global scope are visible and accessible from any part of the program including inside any functions that are defined.

```

  const message = "Hello, world!";
    
  const showMessage = () => {
    console.log(message);
  }
    
  showMessage(); // logs "Hello, world!"
  console.log(message); // logs "Hello, world!"
    
```

In this example, the `message` variable is declared in the global scope and is accessible from within the `showMessage` function. When `showMessage` is called, it logs the value of `message` to the console. It is also accessible outside the function when we tried to log the value of `message` to the console because it was declared outside the function. 

The scope of a variable is determined by the keyword used to declare it. There are three main keywords used to declare variables in JavaScript: `var`, `let`, and `const`.

#### Var
Variables declared with `var` are function-scoped, meaning they are accessible anywhere within the function in which they are defined. If a variable is defined with `var` inside a block (such as a loop or conditional statement), it will still be accessible outside the block, in the function scope.

```
  const example = () => {
    var x = 1;
      
    if (true) {
      var y = 2;
    }
      
    console.log(x); // logs 1
    console.log(y); // logs 2
  }
    
  example();

```    

both `x` and `y` are accessible inside the `example` function, despite `y` being declared inside the `if` block.

#### Let
Variables declared with `let` are block-scoped, meaning they are only accessible within the block in which they are defined. This includes function bodies, loops, and conditional statements.

```
  const example = () => {
    let x = 1;
      
    if (true) {
      let y = 2;
      console.log(x); // logs 1
      console.log(y); // logs 2
    }
      
    console.log(x); // logs 1
    console.log(y); // ReferenceError: y is not defined
  }
    
  example();
   
```

 `x` is accessible both inside and outside the `if` block, while `y` is only accessible within the `if` block. If we try to access `y` outside the `if` block, we get a `ReferenceError`.
 
#### Const

Variables declared with `const` are also block-scoped, but they cannot be reassigned a new value after they are declared. The variable is constant, hence the name `const`. However, it's important to note that when we declare a variable with `const` that points to an object or an array, we can still mutate the object or array.

```
  const example = () => {
    const x = 1;
    x = 2; // TypeError: Assignment to constant variable.
    
    if (true) {
      const y = [1, 2, 3];
      y.push(4);
      console.log(y); // logs [1, 2, 3, 4]
    }
  }
    
  example();
  
```  

we try to reassign `x` to a new value, which results in a `TypeError`. However, we can still add elements to the array `y` that is defined with `const`.

Understanding how variables are scoped in JavaScript is an essential part of writing maintainable and readable code. By using the appropriate keywords (`var`, `let`, `const`) and understanding their scoping rules, developers can avoid naming conflicts, manage data privacy, and write more robust applications.

Closures and scope are similar in that they both deal with the visibility and accessibility of variables and functions in JavaScript. Closures can be seen as a way to "close over" variables and functions from the outer function's scope, creating a private environment that can only be accessed by the closure. This can be useful for creating modular and reusable code that is more secure and less prone to naming conflicts.




## “This” Keyword

The `this` keyword is a special variable in JavaScript that refers to the object that the function is a method of. When a function is called as a method of an object, the `this` keyword inside the function refers to the object that the method is called on.

```
  const person = {
    name: "John",
    age: 30,
    greet: function() {
      console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
    }
  };
    
  person.greet(); // logs "Hello, my name is John and I am 30 years old."
   
```

The `greet` method is defined inside the `person` object, and when it is called using `person.greet()`, the `this` keyword inside the method refers to the `person` object. This allows the method to access the `name` and `age` properties of the object using `this.name` and `this.age`.

The value of `this` can change depending on how the function is called. If a function is called without an explicit context (i.e., without being called as a method of an object), the `this` keyword will refer to the global object (`window` in a browser or `global` in Node.js). This can lead to unexpected behavior and is a common source of bugs in JavaScript code.

To avoid this problem, it is often necessary to bind the `this` keyword to a specific object using the `bind`, `call`, or `apply` methods. These methods allow you to explicitly set the value of `this` when calling a function, ensuring that it always refers to the correct object.
Here's an example of how to use the `bind` method to set the `this` keyword in a function:

```
    const person1 = {
      name: "John",
      age: 30,
      greet: function() {
        console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
      }
    };
    
    const person2 = {
      name: "Jane",
      age: 25
    };
    
    const greetPerson2 = person1.greet.bind(person2);
    
    greetPerson2(); // logs "Hello, my name is Jane and I am 25 years old."
    
```

the `bind` method is used to create a new function `greetPerson2` that has its `this` keyword set to the `person2` object. When `greetPerson2` is called, it uses the `greet` method from `person1`, but with `this` bound to `person2`. This allows the method to access the `name` and `age` properties of `person2` instead of `person`.
 
When a function is called with the `this` keyword, its value is determined by the way the function is called. Here are the four main ways that the value of `this` can be set:


- Global context: When a function is called outside of any object or function, `this` refers to the global object (e.g. `window` in a browser or `global` in Node.js).

- Object context: When a function is called as a method of an object, `this` refers to the object itself.
   
``` 
 const person = {
   name: "John",
   greet: function() {
     console.log("Hello, my name is " + this.name);
   }
 }
    
 person.greet(); // logs "Hello, my name is John"
    
```
the `greet` method is called as a method of the `person` object, so `this` refers to the `person` object.


- Constructor context: When a function is called with the `new` keyword to create a new object, `this` refers to the new object being created.

```
 const Person = (name) => {
   this.name = name;
 }
    
 const john = new Person("John");
    
 console.log(john.name); // logs "John"
    
```

the `Person` function is called with the `new` keyword to create a new object, so `this` refers to the new object being created. The `name` property is then set on the new object.

- Explicit binding: When a function is called using the `call` or `apply` method, `this` is explicitly set to a specific object.

```
 const person1 = { name: "John" };
 const person2 = { name: "Mary" };
    
 const greet = () => {
   console.log("Hello, my name is " + this.name);
 }
    
 greet.call(person1); // logs "Hello, my name is John"
 greet.call(person2); // logs "Hello, my name is Mary"
    
```

the `greet` function is called with the `call` method and the `this` keyword is explicitly set to either `person1` or `person2`. This allows us to reuse the same function with different objects.



## Hoisting

Hoisting is a JavaScript mechanism that allows variables and functions to be used before they are declared. This means that you can declare a variable or function after you use it, and the JavaScript interpreter will still be able to access it. It is a behavior where variable and function declarations are moved to the top of their respective scopes. This means that even if a variable or function is declared later in the code, it can still be used before it is declared.  All variable declarations are "hoisted" to the top of their scope (either the global scope or the scope of a function) and given an initial value of `undefined`. This means that even if you declare a variable at the bottom of a function, you can still use it at the top of the function without causing an error.

For example, consider the following code:

```
 const foo = () => {
   console.log(x);
   const x = 10;
 }
    
 foo();
    
```

Even though `x` is declared after it is used in the `console.log` statement, the code still works because the variable declaration is hoisted to the top of the `foo` function and given an initial value of `undefined`. This means that the `console.log` statement logs `undefined` instead of throwing an error.

Hoisting also applies to function declarations, which are fully hoisted to the top of their scope. This means that you can call a function before it is declared in your code, like this:

```
 foo();
    
 const foo = () => {
   console.log("Hello!");
 }
    
```

In this example, the `foo` function is declared after it is called, but because function declarations are hoisted to the top of the scope, the code still works and logs "Hello!" to the console.

However, it's important to note that hoisting only applies to function and variable declarations, not to function expressions or variable assignments. For example, if you declare a variable using `let` or `const`, you cannot use it before it is declared, like this:

```
 console.log(x); // throws an error
 let x = 10;

```   

It's considered best practice to declare all variables and functions at the top of their scope to avoid confusion and bugs caused by hoisting. By writing clear and readable code, you can ensure that your code works as expected and is easy to maintain over time.



## Destructuring

Destructuring is a way to extract data from arrays and objects in JavaScript. It allows you to assign values to variables in a more concise and readable syntax.

### Destructuring Arrays
To destructure an array, you can use square brackets `[]` on the left-hand side of an assignment to match the structure of the array. It works by matching the positions of the elements in the array with the variables being assigned to it.

```
 const numbers = [1, 2, 3];
 const [a, b, c] = numbers;
    
 console.log(a); // logs 1
 console.log(b); // logs 2
 console.log(c); // logs 3
 
```   

the values in the `numbers` array are destructured into the variables `a`, `b`, and `c`. This is equivalent to the following code:

```
 const numbers = [1, 2, 3];
 const a = numbers[0];
 const b = numbers[1];
 const c = numbers[2];

```   


### Destructuring Objects

To destructure an object, you can use curly braces `{}` on the left-hand side of an assignment to match the keys of the object. Itworks by using the keys of the object to assign values to variables. For example:

```
 const person = { name: "John", age: 30 };
 const { name, age } = person;
    
 console.log(name); // logs "John"
 console.log(age); // logs 30
 
```   

In this example, the values in the `person` object are destructured into the variables `name` and `age`. This is equivalent to the following code:

```
 const person = { name: "John", age: 30 };
 const name = person.name;
 const age = person.age;
 
```   

You can also use default values and aliasing when destructuring objects as well as to destructure nested objects and arrays:

```
 const person = {
   name: "John",
   age: 30,
   address: { city: "New York", state: "NY" },
 };
      
 const {
   name,
   age,
   address: { city, country = "USA" },
 } = person;
    
 console.log(name); // logs "John"
 console.log(age); // logs 30
 console.log(city); // logs "New York"
 console.log(country); // logs "USA"
    
```

In this example, the `address` property of the `person` object is destructured into the variables `city` and `country`, with a default value of "USA" for `country` if it is not present in the `address` object.

Destructuring can help to make code more concise and readable, especially when working with complex data structures. It is also a useful tool for extracting values from arrays and objects in a more intuitive and straightforward way.



## Currying

Currying is a technique in functional programming where a function that takes multiple arguments is transformed into a sequence of functions that each take a single argument. The result of each function call is a new function that takes the next argument in the sequence until all arguments have been processed and the final result is returned. In is simply a technique in JavaScript that involves transforming a function that takes multiple arguments into a series of functions that each take a single argument.

When a curried function is called with a single argument, it returns a new function that expects the next argument. This process can continue until all the arguments have been supplied, at which point the original function is finally called.

To understand currying, let's first look at a function that takes multiple arguments:

```
 const add = (a, b, c) => {
   return a + b + c;
 }
    
 console.log(add(1, 2, 3)); // 6
    
```

Here, the `add` function takes three arguments (`a`, `b`, and `c`) and returns their sum. We can use currying to transform this function into a series of functions that each take a single argument:

```
 const add = (a) => {
   return function(b) {
     return function(c) {
       return a + b + c;
     };
   };
 }
    
 console.log(add(1)(2)(3)); // 6

```   

In this example, we've defined the `add` function using three nested functions. The first function takes the first argument (`a`) and returns a new function that takes the second argument (`b`). The second function returns another new function that takes the third argument (`c`). Finally, the third function returns the sum of all three arguments.

The result of each function call is a new function that takes the next argument in the sequence. This allows us to pass the arguments one at a time, in any order, and still get the correct result.

Currying is useful in situations where you need to create new functions based on existing ones with partially applied arguments. This can simplify code and reduce the amount of redundant code. It can also make code more reusable by creating generic functions that can be used with different arguments.



## Type Coercion

Type coercion is a concept in JavaScript that refers to the automatic conversion of values from one data type to another.
JavaScript is a dynamically typed language, which means that variables can hold values of different types at different times during the execution of a program. However, when an operation is performed on a value of a certain type, JavaScript may automatically convert that value to another type, if necessary, to complete the operation.

For example, when a string is added to a number in JavaScript, the number is automatically converted to a string before the concatenation occurs:

```
 const x = 5;
 const y = '10';
    
 console.log(x + y); // '510'
    
```

In this example, we're adding a number (`5`) to a string (`'10'`). Because the `+` operator can be used for both addition and concatenation in JavaScript, the number `5` is automatically converted to a string (`'5'`) before the concatenation occurs. The result of the operation is a string (`'510'`).

Type coercion can sometimes lead to unexpected behavior and bugs in JavaScript programs, especially when it involves complex expressions or implicit conversions. To avoid these issues, it's often recommended to use explicit type conversion functions, such as `Number()`, `String()`, and `Boolean()`, to convert values to the desired type before performing operations on them:

```
 const x = 5;
 const y = '10';
    
 console.log(x + Number(y)); // 15
 
```   

In this example, we're using the `Number()` function to explicitly convert the string `'10'` to a number before adding it to the number `5`. The result of the operation is a number (`15`), as expected.

JavaScript has two types of coercion: explicit coercion and implicit coercion.

Explicit coercion involves using built-in functions or operators to convert values from one type to another. For example, you can use the `Number()` function to convert a string to a number:

```
 const numString = "123";
 const num = Number(numString);
    
 console.log(typeof num); // "number"
 
```  

In this example, we're using the `Number()` function to explicitly convert a string containing the value `"123"` to a number. We then log the type of the `num` variable, which is `"number"`.

Implicit coercion, on the other hand, happens automatically when JavaScript tries to perform an operation on values of different types. For example, the `+` operator can be used to concatenate strings, but it can also add numbers:

```
 console.log(1 + "2"); // "12"
 console.log("2" + 1); // "21"
 console.log(1 + true); // 2

```   

In these examples, JavaScript is implicitly coercing values of different types to perform the requested operation. In the first example, JavaScript is concatenating a string and a number, resulting in the string `"12"`. In the second example, it's doing the same thing in reverse order. In the third example, JavaScript is coercing the boolean value `true` to a number (`1`) and adding it to the number `1`, resulting in the number `2`.



## IIFE (Immediately Invoked Function Expression)

An IIFE, or Immediately Invoked Function Expression, is a JavaScript function that is executed as soon as it is defined.  It's a commonly used pattern in JavaScript for creating a new scope and avoiding namespace collisions.

The syntax for defining an IIFE is to wrap a function expression in parentheses, followed by another set of parentheses that immediately invoke the function:

```
    (function () {
      // code to be executed immediately
    })();
    
    (() => {
      // code to be executed immediately
    })();
    
    (async () => {
      // code to be executed immediately
    })();
    
```

we're defining an anonymous function expression and immediately invoking it. The function executes as soon as it is defined, without the need for a separate function call.

IIFEs are often used to encapsulate code and prevent it from interfering with other code in the global namespace. For example, if you have a variable with the same name in two different files, it can cause a collision and result in unexpected behavior. By wrapping your code in an IIFE, you can avoid these types of collisions.

IIFEs can also be used to create modules in JavaScript. By returning an object from the IIFE, you can expose only the public properties and methods of the module, while keeping private variables and functions hidden from the global scope:

```
 const myModule = ( () => {
   const privateVar = "Hello, world!";
    
   const privateFunc = () => {
     console.log(privateVar);
   }
    
   return {
     publicVar: "I'm a public variable",
     publicFunc: () => {
       console.log(this.publicVar);
       privateFunc();
     }
   };
 })();
   
 console.log(myModule.publicVar); // "I'm a public variable"
 myModule.publicFunc(); // logs "I'm a public variable" and "Hello, world!"
    
```

In this example, we're defining a module using an IIFE. The module has a private variable called `privateVar` and a private function called `privateFunc`. It also has a public variable called `publicVar` and a public function called `publicFunc`, which calls `privateFunc`.

We then execute the IIFE and assign the resulting object to a variable called `myModule`. We can then access the public properties and methods of the module using the `myModule` variable, while keeping the private variables and functions hidden from the global scope.

IIFEs can also be used to create private variables and functions that are not accessible from outside the function. Here's an example of how this can be done:

IIFEs can also be used to create private variables and functions that are not accessible from outside the function. Here's an example of how this can be done:

```
 const myModule = ( () => {
   const privateVariable = "Hello, world!";
    
   const privateFunction = () => {
     console.log(privateVariable);
   }
    
   return {
     publicFunction: () => {
       privateFunction();
     }
   };
 })();
    
 myModule.publicFunction(); // logs "Hello, world!"
    
```

In this example, we're using an IIFE to create a module with a private variable and function. The private variable and function are not accessible from outside the module, but we've also included a public function that can be called from outside the module. When the public function is called, it calls the private function, which logs the value of the private variable to the console.

IIFEs can be a powerful tool in JavaScript for creating private variables and functions, as well as for avoiding namespace collisions and creating new scopes.



## Prototype Inheritance

Prototype Inheritance is a fundamental concept in JavaScript that allows objects to inherit properties and methods from other objects. In JavaScript, every object has a prototype, which is a reference to another object from which it inherits its properties and methods.
In JavaScript, objects have a prototype property, which points to another object. When you access a property or method on an object and it doesn't exist on that object, JavaScript will look for the property or method on the object's prototype. If the property or method is found on the prototype, it will be used.

To illustrate how prototype inheritance works in JavaScript, let's consider an example. Suppose we have a `Person` object with a `name` property and a `sayHello()` method:

```
 const Person = (name) => {
   this.name = name;
 }
    
 Person.prototype.sayHello = () => {
   console.log("Hello, my name is " + this.name);
 };
    
```

We can create a new `Person` object and call its `sayHello()` method like this:

```
 const person = new Person("John");
 person.sayHello(); // logs "Hello, my name is John"
    
```

Now suppose we want to create a `Student` object that inherits from `Person` and also has a `grade` property:

```
 const Student = (name, grade) => {
   Person.call(this, name);
   this.grade = grade;
 }
    
 Student.prototype = Object.create(Person.prototype);
 Student.prototype.constructor = Student;
    
```

In this example, we're using the `Object.create()` method to create a new object that inherits from `Person.prototype`, which is the prototype of the `Person` object. We're then setting the `constructor` property of the `Student.prototype` object to `Student`.

Now we can create a new `Student` object and call its `sayHello()` method, which is inherited from the `Person` object:

```
 const student = new Student("Jane", 12);
 student.sayHello(); // logs "Hello, my name is Jane"
 console.log(student.grade); // logs 12

```   

In this example, we're able to create a `Student` object that inherits properties and methods from the `Person` object. This allows us to reuse code and create new objects with similar functionality.

Here's another example of how prototype inheritance works:

```
 // create a new object
 const person = {
   name: "John",
   age: 30,
   greet: () => console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old`);
  
 };
    
 // create a new object that inherits from the person object
 const student = Object.create(person);
    
 // add a new property to the student object
 student.major = "Computer Science";
    
 // call the greet method on the student object
 student.greet(); // logs "Hello, my name is John and I'm 30 years old"
    
 // change the name property on the person object
 person.name = "Jane";
    
 // call the greet method on the student object again
 student.greet(); // logs "Hello, my name is Jane and I'm 30 years old"
    
```

In the above code block, we're creating a `person` object with a `name`, `age`, and `greet` property. We then create a new object called `student` that inherits from the `person` object using `Object.create()`. We add a new `major` property to the `student` object.

When we call the `greet()` method on the `student` object, JavaScript first looks for the `greet()` method on the `student` object itself. Since the `greet()` method doesn't exist on the `student` object, JavaScript looks for it on the `person` object, which is the object that the `student` object inherits from.
When we change the `name` property on the `person` object, it affects the `name` property on the `student` object as well. This is because the `student` object is inheriting the `name` property from the `person` object through prototype inheritance.

Prototype inheritance allows you to create objects that share properties and methods, which can help reduce duplication in your code and make your code more modular and maintainable.



## Conclusion

In conclusion, JavaScript is a powerful language with many advanced concepts that can be difficult to understand. In this article, we've explored some of the trickiest concepts in JavaScript, including closures and scope, hoisting, destructuring, currying, type coercion, IIFE, prototype inheritance, and the event loop.
Understanding these concepts is crucial for writing efficient and effective JavaScript code. While they can be challenging to learn, they can also unlock new possibilities for developers and make their code more robust and flexible.
As you continue to improve your skills in JavaScript, don't be afraid to dive deeper into these concepts and explore their nuances. The more you understand about the language, the better equipped you'll be to tackle any coding challenge that comes your way.