20 Common Beginner JavaScript Mistakes and how to fix them

·

34 min read

Table of Content:

  1. Introduction

  2. Foundation: Understanding JavaScript Basics

  3. Mistake #1: Improper Variable Naming

  4. Mistake #2: Mixing up Addition & Concatenation

  5. Mistake #3: Improper Variable Declaration, Reassignment and Scope Management

  6. Mistake #4: Strict Equality (===) vs Equality Operator (==)

  7. Mistake #5: Using the Comparison Operator(==) Instead of the Assignment Operator (=) in JavaScript if Statements

  8. Mistake #6: Loose Comparison with Boolean Values

  9. Mistake #7: Breaking a return Statement

  10. Mistake #8: Omitting the return Statement in Functions

  11. Mistake #9: Omitting the break Statement in Switch Statements

  12. Mistake #10: Not Adding Parenthesis to Methods and Functions

  13. Mistake #11: Poor DOM Manipulation Practices

  14. Mistake #12: Lack of Modularity and Code Organization

  15. Mistake #13: Omitting type="module" in the <script> Tag

  16. Mistake #14: Mutation in Arrays

  17. Mistake #15: Incorrect Method Referencing and Execution

  18. Mistake #16: Arrow Functions and the Binding of this in JavaScript

  19. Mistake #17: Misunderstanding JavaScript's Asynchronous Nature

  20. Mistake #18: Overlooking the Importance of Error Handling

  21. Mistake #19: Inadequate Handling of the "this" Keyword

  22. Mistake #20: Insufficient Understanding of Closures

  23. Conclusion

Introduction:

JavaScript is a popular and influential language in the world of web development. It empowers developers to build dynamic and interactive websites with relative ease, especially for those who have experience in other programming languages. However, beneath its user-friendly facade, JavaScript hides a multitude of pitfalls that can catch developers off guard.

In this article, we will explore some of the most common pitfalls in JavaScript programming. My goal is to empower you with the knowledge and skills needed to navigate these potential pitfalls effectively by offering practical guidance and accompanying it with illustrative code examples. Get ready to unravel the secrets of JavaScript success and elevate your coding skills to new heights!

Foundation:

Let's start at the beginning: the fundamentals. Whether it's in technology or any other field, a solid understanding of the basics is essential for mastery. In JavaScript, variables, data types, functions, and control structures serve as the building blocks of the language. Gaining proficiency in these concepts is vital for writing clean and efficient code. Always keep in mind that mastering the basics is the key to unlocking advanced capabilities in JavaScript.

Mistake #1

Improper Variable Naming:

Naming is hard, I know. However, effective variable naming is vital for writing high-quality code, whether you're working individually or as part of a team. Thoughtfully selecting meaningful and consistent names significantly improves code readability. Let's now explore the issues with poorly named variables, along with best practices for naming variables, including a specific practice for naming arrays.

Explanation:

Poorly named variables can make code difficult to understand and maintain. When variable names are unclear or misleading, it becomes challenging for other developers (including yourself) to comprehend the code's intention. This can lead to bugs, confusion, and increased debugging time.

To illustrate this, let's consider a practical code example that demonstrates both bad and good practices for naming variables.

Bad Practice Example:

const a = 2;
const b = 3;
const c = a + b;

console.log(c); // Output: 5

In this example, the variable names a, b, and c provide no meaningful context about their purpose or the data they hold. This makes it difficult to understand the code's logic, especially in more complex scenarios or when revisiting the code later.

Good Practice Example:

const firstNumber = 3;
const secondNumber = 2;
const sum = firstNumber + secondNumber;

console.log(sum); // Output: 5

In the improved version, the variables are given descriptive names such as firstNumber, secondNumber, and sum. These names clearly convey the purpose and content of each variable, making the code more readable and self-explanatory.

Best Practice for Naming Arrays:

When it comes to naming arrays, a recommended practice is to use the plural form of the data they hold. This helps indicate that the variable represents a collection of items. Additionally, when referring to individual elements within the array, it is advised to use the singular form of the data.

Bad Practice Example:

const fruits = ["apple", "banana", "orange"];

for (let i = 0; i < fruits.length; i++) {
  console.log(fruits[i]); // Output: apple, banana, orange
}

In this example, the array is named fruits, which accurately represents the data it holds. However, when accessing individual elements within the array, the loop variable i is used. The variable name i does not provide any indication of its purpose or its relationship to the elements in the array.

Good Practice Example:

const fruitCollection = ["apple", "banana", "orange"];

for (let fruit of fruitCollection) {
  console.log(fruit); // Output: apple, banana, orange
}

In the improved version, the array is named fruitCollection, using the plural form to denote that it holds multiple fruits. Within the loop, the variable fruit is used to represent each individual fruit in the array. This naming convention improves code clarity by clearly indicating the purpose and context of the variables.

Mistake #2

Mixing Up Addition & Concatenation:

Because concatenation and addition both use the same + operator in JavaScript, it can be easy for beginners to mix them up.

It's important to understand that addition is used for adding numbers, while concatenation is used for joining strings together.

Let's explore this mistake with practical code examples:

Example 1: Addition of Numbers

let num1 = 3;
let num2 = 2;
let result = num1 + num2;

console.log(result); // Outputs: 5

Explanation: When both operands of the + operator are numbers, JavaScript performs addition and returns the sum.

Example 2: Concatenation of Strings

let name = "Luchi";
let message = "Hello, " + name;

console.log(message); // Outputs: Hello, Luchi

Explanation: When one or both operands of the + operator are strings, JavaScript performs string concatenation. It joins the strings together, preserving their order.

Example 3: Mixing Addition and Concatenation

let num = 3;
let text = "2";
let result = num + text;

console.log(result); // Outputs: "32"

In this example, we have a variable num storing a number (2), and a variable text storing a string ("2"). When we use the + operator between these two variables, JavaScript performs string concatenation instead of addition. The number 3 is converted to a string and concatenated with the string "2", resulting in "32".

Explanation: When one operand is a number and the other operand is a string, JavaScript prioritizes string concatenation. The number is implicitly converted to a string, and the two strings are joined together.

To avoid mixing up addition and concatenation, it's crucial to ensure that you are using the appropriate data types. If you want to add numbers, make sure both operands are numbers. If you want to concatenate strings, ensure that both operands are strings.

Mistake #3

Improper Variable Declaration, Reassignment and Scope Management:

JavaScript provides three variable declaration keywords: var, let, and const. Each of these keywords exhibits distinct behaviors in terms of hoisting, scoping rules, and reassignment. Understanding these differences is vital for producing clean and readable code. Let's delve into these concepts and glean valuable insights to empower you in confidently handling variable declaration in JavaScript.

const variable: The const keyword is used to declare a variable that remains constant and cannot be reassigned after initialization. It provides immutability within its scope.

const message = "Hello, World!";
message = "Goodbye, World!"; // Error: Assignment to constant variable.

In the above example, we declare a const variable named message and assign it the initial value of "Hello, World!". When we attempt to reassign a new value to message, such as "Goodbye, World!", it throws a TypeError, as it violates the immutability enforced by const.

let: The let keyword is used to declare a variable that can be reassigned within its block scope. It allows flexibility in changing the value of the variable.

let count = 5;
count = 10; // No error, variable successfully reassigned.

In this example, the let keyword is used to declare the variable count. We can assign a new value to count, such as 10, without any errors. let provides reassignability within its block scope.

var: The var keyword is used to declare a variable that can be reassigned within its block or global scope.

var count = 1;
console.log(count); // Output: 1

count = 2;
console.log(count); // No error, variable successfully reassigned. Output: 2

In this example, we declare a variable named count using var and assign it an initial value of 1. We then reassign the variable with a new value of 2. The reassignment is allowed because var variables can be modified within their function scope or global scope. As a result, the output of the console.log statements reflects the updated value of count.

Variable declaration and scope management: Here's a practical code example to illustrate the importance of proper variable declaration and scope management:

function example() {
    var x = 10;
    let y = 20;

    if (true) {
        var x = 30; // Overrides the outer variable
        let y = 40; // Creates a new block-scoped variable

        console.log(x, y); // Output: 30 40
    }

    console.log(x, y); // Output: 30 20
}

example();

In the code example, we have variables declared using both var and let. The var declaration creates a global variable, while the let declaration creates a variable limited to the scope it's defined in.

In the if block, we see that the var declaration for x overrides the outer variable, meaning it changes the value of the x variable globally. On the other hand, the let declaration for y creates a new variable that is specific to the block scope of the if statement.

The console.log statements show the values of x and y at different points in the code. Inside the if block, the values are 30 and 40 respectively. Outside the if block, the value of x remains 30 (due to variable hoisting) while y remains 20.

It's important to understand the difference between var, let, and const declarations and use them appropriately to avoid unintended variable reassignment and maintain predictable code behavior.

Note: It's generally recommended to use let and const for variable declarations instead of var to mitigate this particular issue and improve code clarity and predictability.

Mistake #4

Strict Equality (===) vs. Equality Operator (==)

In JavaScript, there are two comparison operators for equality: the strict equality operator (===) and the equality operator (==). While both operators are used to compare values, they differ in their behavior and level of strictness. Understanding the distinction between them is crucial to writing reliable and predictable code. Let's explore the differences between strict equality and the equality operator, along with their use cases and best practices.

Explanation:

  1. Strict Equality (===): The strict equality operator (===) compares two values for equality without performing any type coercion. It checks both the value and the type of the operands. The result is true only if the values are equal and of the same type.
console.log(3 === 3); // Output: true
console.log(3 === '3'); // Output: false

In the first example, 3 === 3 returns true because both operands are numbers and have the same value. However, in the second example, 3 === '3' returns false since the operands have different types (number vs. string), even though they have the same value.

Strict equality is considered more reliable as it ensures both value and type consistency in comparisons. It is often recommended to use strict equality when comparing values to avoid unexpected type coercion.

Equality Operator (==): The equality operator (==) also compares two values for equality but allows type coercion. It attempts to convert the operands to the same type before making the comparison. This can lead to unexpected and sometimes counterintuitive results.

console.log(3 == 3); // Output: true
console.log(3 == '3'); // Output: true

In the first example, 3 == 3 returns true since both operands are numbers with the same value. In the second example, 3 == '3' also returns true because the equality operator coerces the string into a number before comparison.

Using the equality operator can be convenient in certain situations, as it allows for flexible type comparisons. However, it can also introduce subtle bugs and make code harder to understand, especially when dealing with different types and potential type coercion.

Best Practice:

To write more reliable and less error-prone code, it is generally recommended to use the strict equality operator (===) over the equality operator (==) whenever possible. By using strict equality, you ensure that comparisons are performed with both value and type consistency, reducing the chances of unexpected behavior.

By adhering to this best practice, you can write code that is more explicit and easier to understand, as it avoids implicit type conversions and potential pitfalls associated with type coercion.

Note: Strict mode and TypeScript are fantastic ways of ensuring you never have to worry about this.

Mistake #5

Using Comparison Operators Instead of the Assignment Operator in JavaScript's "if" Statements:

As a beginner programmer, you may find yourself accidentally using the assignment operator (=) instead of the comparison operator (== or ===) in an if statement. This mistake can lead to unexpected and incorrect program behavior.

Let's delve into this issue with code examples to understand it better:

Example 1:

let x = 3;
if (x = 10) {
  // Code block executed if x is assigned the value 10
  console.log("x is assigned the value 10");
}

In this example, the programmer mistakenly uses the assignment operator (=) instead of the comparison operator. This mistake causes the if statement to return true, regardless of the initial value of x. The assignment expression x = 10 assigns the value 10 to x and returns that value, which is considered truthy. As a result, the code block inside the if statement is executed, even though it may not be the intended behavior.

To avoid this mistake, always use the appropriate comparison operator (== or ===) when comparing values in if statements:

Example 2:

let x = 3;
if (x === 3) {
  // Code block executed if x is exactly equal to 3 (including type)
  console.log("x is exactly equal to 3");
}

By using the strict equality operator (===), we ensure that both the value and the type of x are compared to 3. This helps prevent accidental assignments and promotes expected and reliable program behavior.

Mistake #6

Loose Comparisons with Boolean Values:

In JavaScript, it's common to check boolean values using conditional statements. However, there are certain pitfalls when performing loose comparisons that can lead to unexpected behavior. Let's examine these scenarios with practical code examples to understand the potential issues.

Checking Boolean Values:

const isFirstName = true;

if (isFirstName) {
    console.log('Luchi');
} else {
    console.log('This isn't your first name');
}
// Output: Luchi

In this example, the boolean value of isFirstName is checked using an if statement. If isFirstName is true, the message "Luchi" is logged; otherwise, the message "This isn't your first name" is logged. This is a straightforward and correct usage of boolean checking.

Loose Comparison Pitfall:

const obj = {
    name: 'Luchi',
    id: 0
};

if (obj.id) {
    console.log('id property exists');
} else {
    console.log('id property does not exist');
}
// Output: id property does not exist

In this example, the obj object has a property named id with a value of 0. When checking the obj.id property, a loose comparison is performed. JavaScript's loose comparison treats 0 as a falsy value, leading to the else block being executed. This can be misleading because the property actually exists, but the value is considered falsy.

To avoid such pitfalls, it's recommended to use strict comparisons (=== or !==) when working with boolean values or properties in objects. Here are a couple of best practices:

  • Check boolean values using strict comparison:
if (a === false) {
    // ...
}
  • Check object properties using appropriate methods:
if (object.hasOwnProperty(property)) {
    // ...
}

Example:

const obj = {

    name: 'Luchi',
    id: 0
};

if (obj.hasOwnProperty('id') && obj.id) {
     // Check if the 'id' property exists and has a truthy value
    console.log('id property exists');
} else {
    console.log('id property does not exist');
}

// Output: id property exists

By using strict comparisons and specific methods, you can ensure accurate evaluations of boolean values and object properties, preventing unexpected outcomes and promoting code clarity and reliability.

Mistake #7

Breaking a return Statement:

In JavaScript, it is a default behavior for the language to automatically insert semicolons at the end of a line if they are missing. This behavior can lead to unexpected results when using return statements.

Let's explore this mistake with practical code examples:

Example 1:

function myFunction() {
  const
     // The following line has a broken string declaration
    // However, the code still runs since JavaScript allows breaking the string declaration line
  name = "Luchi"; 
  return name;
}

console.log("Hi " + myFunction()); // Outputs: Hi Luchi

Explanation: In JavaScript, you can break a line within a statement as long as the statement is not broken across multiple lines. In this example, we declare a constant variable name and assign it the value "Luchi". Breaking the line after the const keyword is valid, and JavaScript will interpret it correctly. The return statement is written on the same line as the variable name, so it will return the value "Luchi" as expected.

Example 2:

function myFunction() {
  const name = "Luchi"; 
  return
  name;
}

console.log("Hi " + myFunction()); // Outputs: Hi undefined

In this example, we have a similar function called myFunction. However, the return statement is broken and placed on a new line after the return keyword.

Due to automatic semicolon insertion, JavaScript interprets the code as follows:

function myFunction() {
  let name = "Luchi"; 
  return; // A semicolon is automatically inserted here
  name;
}

console.log("Hi " + myFunction()); // Outputs: Hi undefined

In this case, the return statement does not have any value, so it returns undefined. The subsequent name; expression is never executed. Therefore, the console output will be "Hi undefined".

Explanation: Unlike strings, where JavaScript allows breaking them across multiple lines, breaking a return statement by placing it on a new line can lead to unintended consequences. Automatic semicolon insertion occurs, and a semicolon is automatically inserted after the return statement, effectively breaking it. The code on the next line is not considered part of the return statement and is not executed. As a result, the return value becomes undefined.

Mistake #8

Omitting the Return Statement in Functions:

If you encounter a situation where a function call returns undefined, it is likely due to this oversight. In JavaScript, functions default to returning undefined if the return keyword is not explicitly used to return a value.

Let's examine this mistake with a practical code example and an improved solution:

// Incorrect example
const getAddedValue = (a, b) => {
  a + b;
};

In the above code, the getAddedValue function is intended to calculate the sum of two numbers a and b. However, the function implementation mistakenly only performs the addition operation but does not explicitly return the result. As a result, when calling getAddedValue, the function returns undefined instead of the expected sum.

To rectify this mistake, ensure that you explicitly return the desired result using the return keyword:

// Corrected example
const getAddedValue = (a, b) => {
  return a + b;
};

In the corrected code, the getAddedValue function now includes the return keyword before the addition operation. This ensures that the calculated sum of a and b is returned as the result of the function.

By including the return statement, the function now correctly returns the expected result when called:

console.log(getAddedValue(3, 5)); // Output: 8

Mistake #9

Omitting the break Statement in Switch Statements:

In JavaScript, the switch statement is a control flow construct used to perform different actions based on different conditions. One common mistake made by programmers is forgetting to include the break statement within each case block of the switch statement.

Consider the following JavaScript code snippet:

const fruit = "apple";

switch (fruit) {
  case "apple":
    console.log("Selected fruit: apple");
  case "banana":
    console.log("Selected fruit: banana");
  case "orange":
    console.log("Selected fruit: orange");
  default:
    console.log("Unknown fruit");
}

In this code, we have a switch statement that evaluates the value of the fruit variable. Based on the value, it executes the corresponding case block.

However, a common mistake made by programmers is to forget to include the break statement at the end of each case block.

When the switch statement is executed, it will match the value of fruit with the first case ("apple"). Since there is no break statement after the console.log statement, the execution will continue to the next case block ("banana") and subsequently to the following case block ("orange"). This behavior is known as "falling through" the cases.

As a result, if fruit is equal to "apple", the output will be:

Selected fruit: apple
Selected fruit: banana
Selected fruit: orange
Unknown fruit

Even though the value matches the "apple" case, all subsequent case blocks are also executed because there are no break statements to terminate the execution of the switch statement. This unintended behavior can lead to incorrect program logic and unexpected results.

To ensure that each case block is executed independently and prevent falling through to subsequent cases, it is crucial to include the break statement at the end of each case block:

const fruit = "apple";

switch (fruit) {
  case "apple":
    console.log("Selected fruit: apple");
    break;
  case "banana":
    console.log("Selected fruit: banana");
    break;
  case "orange":
    console.log("Selected fruit: orange");
    break;
  default:
    console.log("Unknown fruit");
    break;
}

By including the break statement, the execution of the switch statement will terminate after each case block is executed, preventing any unintended fall-through behavior.

Now, when the fruit variable is equal to "apple", the output will be:

Selected fruit: apple

The execution will stop after the matching case is executed, and subsequent cases will be skipped.

It is important to note that omitting the break statement might be intentional in some cases. For example, if you want multiple cases to execute the same code block, you can omit the break statement to achieve that behavior. This concept is known as "falling through" intentionally.

const day = 3;
let dayName;

switch (day) {
  case 1:
  case 2:
  case 3:
  case 4:
  case 5:
    dayName = "Weekday";
    break;
  case 6:
  case 7:
    dayName = "Weekend";
    break;
  default:
    dayName = "Unknown day";
    break;
}

console.log(dayName); // Output: Weekday

In this example, the switch statement groups multiple cases (1, 2, 3, 4, 5) together to execute the same code block. If any of these cases match, the dayName variable will be assigned the value "Weekday". The break statement is included after the code block to terminate the execution of the switch statement.

To summarize, when working with switch statements in JavaScript, always remember to include the break statement at the end of each case block. This ensures that only the matching case is executed and prevents unintended fall-through behavior. However, intentional fall-through can be achieved by omitting the break statement, allowing multiple cases to execute the same code block.

Mistake #10

Not Adding Parentheses to Methods and Functions:

When I just started learning JavaScript, the most common mistake I made was omitting parentheses when invoking functions or methods. This mistake unintentionally references the function or method itself instead of executing it.

Consider the following JavaScript code snippet:

function greet(name) {
  console.log(`Hello, ${name}!`);
}

const person = {
  name: "Luchi",
  sayHello() {
    greet(this.name);
  }
};

In this code, we have a simple function greet() that logs a greeting to the console, and an object person with a name property and a sayHello method that invokes the greet function and passes in the name property with the this keyword.

Now, if we want to call the sayHello method on the person object, it is essential to include parentheses after the method name:

person.sayHello();  // Correct usage: Invoke the sayHello method with ()

However, a common mistake made by new JavaScript programmers is to forget the parentheses, like this:

person.sayHello;   // Incorrect usage: Omitting parentheses can lead to unintended consequences

In this incorrect usage, instead of invoking the sayHello method, we are referring to the method itself without executing it. This can be problematic because the expected behavior of executing the method is lost.

Note: There are cases where omitting parentheses may be acceptable. One such scenario is when you want to pass the function or method itself as an argument to another function. In such cases, you can pass the function reference without the parentheses, allowing the receiving function to execute it when needed. This pattern is commonly used in event handlers or callback functions. Here's an example:

function performOperation(operation) {
  console.log("Performing operation...");
  operation(); // Execute the operation function
}

function sayHi() {
  console.log("Hi!");
}

performOperation(sayHi); // Pass the sayHi function without parentheses

In this example, the performOperation function accepts a function as an argument (operation). When performOperation is called and the sayHi function is passed as an argument without parentheses, it is stored in the operation parameter. Later, within performOperation, the operation function is executed by calling it as operation(). This allows for dynamic execution of different functions depending on what is passed.

Mistake #11

Poor DOM Manipulation Practices - Loading JavaScript Scripts in HTML Before the DOM Is Loaded**:

A very common mistake beginners make is loading JavaScript Scripts in HTML Before the DOM(Document Object Model) Is Loaded. This can cause errors when the rest of the page is loaded but the script hasn't executed yet, leading to unexpected behavior. To avoid this, you can either move the script tag to the bottom of the page or use the "defer" attribute when declaring it in the head.

Example 1: Placing the script tag at the bottom of the page

<html>
  <head>
    <!-- Your CSS and other resources -->
  </head>
  <body>
    <!-- Your HTML content -->

    <script src="your-script.js"></script>
  </body>
</html>

Placing the script tag at the bottom ensures that the HTML content is fully loaded before the JavaScript code is executed, reducing the likelihood of errors.

Example 2: Using the "defer" attribute in the script tag

<html>
  <head>
    <!-- Your CSS and other resources -->
    <script src="your-script.js" defer></script>
  </head>
  <body>
    <!-- Your HTML content -->
  </body>
</html>

Using the "defer" attribute tells the browser to defer the execution of the script until the HTML content is parsed, allowing the page to load without blocking. This ensures that the script is executed in the order it appears in the HTML, avoiding any potential errors caused by dependencies.

By implementing either of these approaches, you can ensure that your JavaScript code is executed at the appropriate time, preventing errors and enabling smooth functioning of your web page.

Mistake #12

Lack of Modularity and Code Organization:

Keep your JavaScript code modular(break into smaller blocks) and organized. Avoid large, monolithic code structures that make it hard to reuse code and collaborate with others.

Instead, break your code into smaller, reusable functions or components, and use JavaScript modules or classes. This approach promotes code reusability, improves organization, and makes it easier to work together with other developers.

Let's consider a practical code example showcasing modular code organization using JavaScript modules:

// File: mathUtils.js

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

// File: main.js

import { add, subtract } from "./mathUtils.js";

console.log(add(5, 3)); // Output: 8
console.log(subtract(10, 4)); // Output: 6

In this example, we define two functions (add and subtract) in a separate module called mathUtils.js. We then import and use these functions in our main.js file. This modular approach improves code organization, encourages code reuse, and facilitates easier maintenance and collaboration.

Mistake #13

Omitting type="module" in the <script> Tag

A common beginner pitfall to keep in mind when working with JavaScript modules is omitting the type="module" attribute in the <script> tag. The type="module" attribute is essential for specifying that a JavaScript file should be treated as a module and allows you to take advantage of module features such as import/export statements.

Consider the following code snippet:

<!DOCTYPE html>
<html>
  <head>
    <title>Module Example</title>
  </head>
  <body>
    <script src="script.js"></script>
  </body>
</html>

In this example, we have an HTML file that includes a <script> tag referencing an external JavaScript file named script.js. However, the type="module" attribute is missing from the <script> tag. Let's explore the implications of this omission.

Code Example 1: Module without type="module"

In script.js, let's assume we have the following code:

// script.js
const greeting = "Hello, world!";

export default greeting;

When the HTML file is loaded in the browser, without the type="module" attribute, the JavaScript file script.js is treated as a classic script rather than a module. Consequently, attempting to use features such as export statements will result in an error, and the code won't work as expected.

Code Example 2: Module with type="module"

Now, let's correct the mistake and add the type="module" attribute to the <script> tag:

<!DOCTYPE html>
<html>
  <head>
    <title>Module Example</title>
  </head>
  <body>
    <script type="module" src="script.js"></script>
  </body>
</html>

With the addition of type="module", the JavaScript file script.js is treated as a module. The module code can now use export statements to export variables, functions, or classes for use in other modules.

// script.js
const greeting = "Hello, world!";

export default greeting;

In this corrected example, the module exports a variable greeting using the export default syntax. To demonstrate the usage, let's create another module called main.js and import the exported value from script.js:

// main.js
import greeting from './script.js';

console.log(greeting); // Output: Hello, world!

In main.js, we use the import statement to import the greeting variable from the script.js module. We then log the value of greeting to the console, which should be "Hello, world!".

When you load the HTML file in a browser with the corrected code, the JavaScript module main.js will be executed, and it will successfully import the greeting variable from the script.js module. The expected output "Hello, world!" will be logged to the console.

Mistake #14

Mutation in Arrays:

In JavaScript, arrays are mutable(meaning modifiable) data structures, allowing for the modification of their elements. It's important to understand how different array methods can affect the original array and how to create exact copies without modifying the original.

Let's explore practical code examples using array methods such as map, sort, and array spreading to showcase the effects of mutation and non-mutation on arrays.

Mutation Using sort Method: The sort method is commonly used to arrange the elements of an array in a specified order. However, it directly mutates the original array.

let numbers = [3, 1, 2];
let sortedNumbers = numbers.sort();
console.log(numbers); // Output: [1, 2, 3]

In this example, the sort method is applied to the numbers array. The original array is modified, and subsequent operations will reflect the sorted order.

Non-Mutation Using Array Spreading: Array spreading is a non-mutating technique that allows you to create an exact copy of an array by expanding its elements.

const originalArray = [3, 1, 2];
const newArray = [...originalArray];
let sortedArray = originalArray.sort();
console.log(newArray); // Output: [3, 1, 2]
console.log(originalArray); // Output: [1, 2, 3]

In this example, we create a new array newArray by using the spread operator ([...originalArray]), which makes a copy of the originalArray. The originalArray is then sorted in ascending order using the sort() method.

When we log newArray, it outputs [3, 1, 2] because it is a copy of the original unsorted array. However, logging originalArray outputs [1, 2, 3] since it has been sorted in-place.

In summary, the spread operator creates a copy of the array leaving the original unchanged, while the original array is modified by the sort() method.

It's important to note that the spread operator ([...originalArray]) creates a shallow copy of the array, meaning that if the array contains objects or nested arrays, those nested elements will still reference the same objects or arrays in memory.

Non-Mutation Using map Method: The map method creates a new array by applying a transformation to each element of the original array. It leaves the original array intact.

const numbers = [1, 2, 3];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // Output: [2, 4, 6]

In this example, the map method is used to iterate over each element in the numbers array and create a new array, doubledNumbers, with each element multiplied by 2. The original array, numbers, remains unmodified.

By understanding the effects of different array methods, you can choose the appropriate approach based on whether you want to mutate the original array or create a new array without modifying the original.

Remember, when you need to modify the original array, methods like sort can be useful. On the other hand, if you want to create a copy or transform the original array while preserving it, non-mutating techniques like array spreading ([...]) or using methods such as map are preferred.

Mistake #15

Incorrect Method Referencing and Execution:

In JavaScript, when referencing instance methods of an object, it's important to be mindful of the context in which the method is being called. One common mistake is creating incorrect references to instance methods, resulting in the loss of the correct context and potential errors. Let's explore this issue in detail with practical code examples.

Incorrect Reference Example:

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const person = new Person("Luchi");

// Incorrect reference to the greet method
const incorrectGreet = person.greet;
incorrectGreet(); // Output: TypeError: Cannot read property 'name' of undefined

In this example, we have a Person class with a greet method that logs a greeting containing the person's name. However, when the greet method is assigned to the incorrectGreet variable and invoked, an error occurs. This happens because the incorrectGreet loses the reference to the instance of the Person object, resulting in this being undefined. Consequently, accessing this.name throws a TypeError.

To avoid this issue, it's crucial to maintain the correct reference to instance methods. There are a few ways to do this:

Use arrow functions: Arrow functions don't bind their own this context, instead adopting the context of the surrounding scope. Therefore, they can be used to maintain the correct reference to instance methods.

class Person {
  constructor(name) {
    this.name = name;
  }

  greet = () => {
    console.log(`Hello, my name is ${this.name}`);
  };
}

const person = new Person("Luchi");

const correctGreet = person.greet;
correctGreet(); // Output: Hello, my name is Luchi

In this updated example, the greet method is defined as an arrow function within the class. Arrow functions ensure that this retains the correct reference to the instance of the Person object, allowing the method to be invoked successfully.

Use bind to bind the correct context explicitly: The bind method can be used to bind the correct this context explicitly to the method.

class Person {
  constructor(name) {
    this.name = name;
    this.greet = this.greet.bind(this);
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const person = new Person("Luchi");

const correctGreet = person.greet;
correctGreet(); // Output: Hello, my name is Luchi

In this example, this.greet.bind(this) ensures that the greet method retains the correct this context, even when referenced as correctGreet.

By using arrow functions or explicitly binding the correct context with bind, you can avoid creating incorrect references to instance methods and ensure that this points to the appropriate object instance. This allows you to invoke the methods correctly and access the instance properties within them.

Mistake #16

Arrow Functions and the Binding of 'this' in JavaScript :

Arrow functions do not have their own this binding and instead captures the this value from its surrounding lexical context.

In most cases, this surrounding context will be the object or function that contains the arrow function.

However, there is one particular scenario where an arrow function can result in undefined for this. This occurs when the arrow function is not defined within any object or function, but rather at the top level scope (global scope) or inside a non-arrow function.

Here's an example to illustrate this:

const person = {
  name: "Luchi",
  regularGreet: function() {
    console.log("Regular greet:", this.name);
  },
  arrowGreet: () => {
    console.log("Arrow greet:", this.name);
  },
};

person.regularGreet(); // Output: Regular greet: Luchi

person.arrowGreet(); // Output: Arrow greet: undefined

In the above code, the arrowGreet method is defined as an arrow function. Since the arrow function is not defined within any specific object or function, it captures the this value from its surrounding lexical scope, which is the global scope (in a browser environment, it would be the window object). In the global scope, this refers to the global object, which is typically window in a browser environment. However, since arrow functions do not have their own this binding, this.name will be undefined.

So, in this specific case, the output of person.arrowGreet() will be "Arrow greet: undefined".

It's important to note that this behavior applies specifically to arrow functions and not to regular functions, which have their this binding determined by how they are called.

To fix the example we simply turn the arrow function to a regular function. This ensures that the function has its own this context, which will be the person object in this case. Now, both regularGreet and arrowGreet will correctly access the name property of the person object and output the expected result.

Corrected code:

const person = {
  name: "Luchi",
  regularGreet: function() {
    console.log("Regular greet:", this.name);
  },
  arrowGreet: function() {
    console.log("Arrow greet:", this.name);
  },
};

person.regularGreet(); // Output: Regular greet: Luchi
person.arrowGreet(); // Output: Arrow greet: Luchi

Mistake #17

Misunderstanding JavaScript's Asynchronous Nature:

In the simplest terms, asynchronous in JavaScript refers to actions that happen non-sequentially. Unlike synchronous operations that occur one after the other, JavaScript operates in an asynchronous and event-driven manner.

This means that code execution can occur concurrently and is driven by events, allowing for efficient handling of multiple tasks at the same time.

Beginner developers often struggle with asynchronous operations and callbacks, which can cause bugs and unpredictable behavior. To make things easier, you can use promises and async/await syntax. Promises provide a structured way to handle asynchronous code, while async/await simplifies the syntax and makes the code easier to read.

Let's look at a practical example:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Data fetched successfully!");
        }, 3000);
    });
}

async function getData() {
    try {
        const result = await fetchData();
        console.log(result);
    } catch (error) {
        console.error("Error:", error);
    }
}

getData();

This code fetches data in an asynchronous manner using promises and async/await. Here's how it works:

  1. The fetchData function represents a task that takes some time to complete. It returns a promise, which is like a special object that holds the result of the task in the future. In this case, the promise will resolve after a 3-second delay and provide the message "Data fetched successfully!"

  2. The getData function is marked with the async keyword, which means it contains asynchronous operations. Inside this function, the await keyword is used to pause the execution until the promise returned by fetchData is resolved. Once the promise is resolved, the result is assigned to the result variable.

  3. If the promise is resolved successfully, the result value is logged to the console.

  4. If an error occurs during the execution of the promise, it is caught in the catch block, and an error message is logged to the console.

  5. Finally, the getData function is called, initiating the process of fetching data asynchronously.

Simply put, this code fetches data in an asynchronous way and logs the result. It also handles any errors that may occur during the data fetching process.

Mistake #18

Overlooking the Importance of Error Handling :

Proper error handling is crucial for writing robust and reliable JavaScript code. Beginners often overlook the significance of implementing effective error handling, leading to unhandled exceptions and hard-to-debug issues.

To address this, make error handling an integral part of your codebase. Use try-catch blocks to encapsulate potentially error-prone code and gracefully handle exceptions. This way, you can catch errors, log meaningful messages, and prevent them from disrupting the flow of your application.

Consider the following practical code example:

try {
    // Code that might throw an error
    throw new Error("Something went wrong!");
} catch (error) {
    console.error("Error:", error.message);
}

In this example, we deliberately throw an error using the throw statement. The try-catch block catches the error, and the error.message property provides a descriptive error message for debugging purposes.

Mistake #19

Inadequate Understanding of the Context of the "this" Keyword :

The "this" keyword in JavaScript can be confusing for beginners. It has a dynamic nature, and using it improperly can cause unexpected errors and behavior.

To ensure the correct context, it's important to understand how "this" behaves in different situations. Techniques like bind(), call(), and apply() can help with managing the "this" keyword effectively.

Consider the following practical code example:

const person = {
  name: "Luchi",
  greet: function(greeting) {
    console.log(greeting + ", " + this.name);
  },
};

const friend = {
  name: "Ada",
};

// Using bind()
const boundGreet = person.greet.bind(friend);
const boundGreet2 = person.greet();
boundGreet2('Hi')
boundGreet("Hello"); // Output: Hello, Ada

// Using call()
person.greet.call(friend, "Hi"); // Output: Hi, Ada

// Using apply()
person.greet.apply(friend, ["Hey"]); // Output: Hey, Ada

Explanation:

    1. We define an object person with a name property and a greet method that logs a greeting message using the name property of the object.

      1. We create another object friend with its own name property.

      2. Using bind(), we create a new function boundGreet that binds the greet method of person to the friend object. This means that whenever boundGreet is invoked, the this keyword inside the greet method will refer to the friend object. We pass the greeting "Hello" as an argument when calling boundGreet(), and the output will be "Hello, Ada".

      3. Using call(), we directly invoke the greet method of person, but with the friend object as the context (this). We pass the greeting "Hi" as the first argument to call(), and the output will be "Hi, Ada".

      4. Using apply(), we invoke the greet method of person similar to call(), but with the friend object as the context. The difference is that we pass the arguments as an array instead of separate arguments. In this case, we pass ["Hey"] as the argument array, and the output will be "Hey, Jane".

In summary, bind() creates a new function with a specific context, call() invokes a function with a specified context and individual arguments, and apply() invokes a function with a specified context and an array of arguments. These techniques allow you to control the this value and arguments when calling a function, providing flexibility and versatility in JavaScript programming.

Mistake #20

Insufficient Understanding of Closures:

Closures occur when an inner function has access to variables from its outer enclosing function, even after the outer function has finished executing. They are handy for creating private variables and functions that can remember and maintain their values even after they are no longer in use.

Closures are a powerful concept in JavaScript, however, they can be challenging for beginners to comprehend fully. A lack of understanding regarding closures often leads to unintended variable references and memory leaks.

Let's explore a practical example to illustrate closures:

function outer() {
    const message = "Hello";

    function inner() {
        console.log(message);
    }

    return inner;
}

const closureExample = outer();
closureExample(); // Output: Hello

In this example, the outer function defines the message variable and declares the inner function. When we invoke outer, it returns the inner function. Even though the outer function has finished executing, the closureExample function still has access to the message variable due to the closure. Invoking closureExample logs the message "Hello" to the console.

Understanding closures enable you to utilize them for powerful programming techniques such as encapsulation, data privacy, and creating higher-order functions.

Conclusion:

In this comprehensive guide, we've explored the common mistakes that beginners often make in JavaScript. This list is by no means exhaustive but we covered enough ground to set you on the road to Javascript mastery.

By understanding and avoiding the pitfalls above listed, you can enhance your JavaScript skills, write cleaner code, and overcome challenges more effectively.

Remember, you only get good by doing so get your hands on that keyboard and build something today. Embrace the lessons you learned here as you embark on your journey to JavaScript greatness! I'm rooting for you!

Until next time, byeeeeee!!!