Top 10 JavaScript Interview Questions and Answers (2024)

JavaScript, created by Brendan Eich in 1995, is one of the most widely used web development languages. Here are the common JavaScript Interview Questions asked during interview.

JavaScript Interview Questions for 5 years experience

1. Explain the difference between arrow functions and regular functions in terms of their handling of this context.

Answer: Arrow functions do not have their own this context; instead, they inherit this from the surrounding lexical scope where they are defined. Regular functions, on the other hand, have a dynamic this context determined by how they are called.

// Regular function
const regularObj = {
    name: 'Regular Object',
    regularFunction: function() {
        console.log('Inside regularFunction:', this.name);
    }
};

// Arrow function
const arrowObj = {
    name: 'Arrow Object',
    arrowFunction: () => {
        console.log('Inside arrowFunction:', this.name);
    }
};

regularObj.regularFunction(); // Output: "Inside regularFunction: Regular Object"

// In this case, inside arrowFunction, 'this' refers to the global scope
// Because arrow functions inherit 'this' from the surrounding lexical scope
arrowObj.arrowFunction(); // Output: "Inside arrowFunction: undefined"

In this example:

  • regularObj has a method regularFunction that is a regular function. When regularFunction is called using regularObj.regularFunction(), this inside the function refers to the regularObj object. Therefore, it correctly logs "Inside regularFunction: Regular Object".
  • arrowObj has a method arrowFunction that is an arrow function. When arrowFunction is called using arrowObj.arrowFunction(), this inside the function refers to the global scope because arrow functions inherit this from the surrounding lexical scope. Therefore, it logs "Inside arrowFunction: undefined" because this.name refers to the name property of the global object, which is undefined in this context.

2. How do arrow functions behave differently from regular functions when accessing variables in their lexical scope?

Answer: Arrow functions capture the value of variables from their surrounding lexical scope at the time of their definition. In contrast, regular functions resolve variables dynamically based on their invocation context.

// Using a regular function
function regularFunction() {
    const localVar = 'I am a local variable';
    const innerFunction = function() {
        console.log(localVar);
    };
    return innerFunction;
}

const regularFunc = regularFunction();
regularFunc(); // Output: "I am a local variable"

// Using an arrow function
function arrowFunction() {
    const localVar = 'I am a local variable';
    const innerFunction = () => {
        console.log(localVar);
    };
    return innerFunction;
}

const arrowFunc = arrowFunction();
arrowFunc(); // Output: "I am a local variable"

In this example:

  • In the regularFunction, innerFunction is a regular function. When innerFunction is executed (regularFunc()), it correctly logs the value of localVar, which is 'I am a local variable'. This is because regular functions resolve variables dynamically based on their invocation context.
  • In the arrowFunction, innerFunction is an arrow function. When innerFunction is executed (arrowFunc()), it also correctly logs the value of localVar, which is 'I am a local variable'. This is because arrow functions capture the value of variables from their surrounding lexical scope at the time of their definition. So, even though innerFunction is executed outside of the scope of arrowFunction, it still retains access to localVar.

3. Describe the role of arrow functions in maintaining context in JavaScript.

Answer: Arrow functions are useful for maintaining context by capturing this from their surrounding lexical scope. This behavior helps in scenarios where you want to preserve the context of this from the enclosing code.

// Using a regular function
const obj = {
    name: 'Regular Object',
    regularFunction: function() {
        setTimeout(function() {
            console.log('Inside regularFunction:', this.name);
        }, 1000);
    }
};

obj.regularFunction(); // Output: "Inside regularFunction: undefined" (or an error)

// Using an arrow function
const arrowObj = {
    name: 'Arrow Object',
    arrowFunction: function() {
        setTimeout(() => {
            console.log('Inside arrowFunction:', this.name);
        }, 1000);
    }
};

arrowObj.arrowFunction(); // Output: "Inside arrowFunction: Arrow Object"

In this example:

  • obj has a method regularFunction that is a regular function. Inside regularFunction, a setTimeout function is used to log this.name after a delay. However, inside the callback function passed to setTimeout, this does not refer to obj anymore due to the function context being changed by setTimeout. As a result, this.name is undefined, or it may throw an error if strict mode is enabled.
  • arrowObj has a method arrowFunction that is a regular function. Inside arrowFunction, a setTimeout function is used with an arrow function as the callback. Since arrow functions capture this from their surrounding lexical scope (arrowFunction in this case), this.name inside the arrow function still refers to the name property of arrowObj. Therefore, it correctly logs "Inside arrowFunction: Arrow Object" after the delay.

4. Explain how arrow functions contribute to avoiding context-related bugs in nested functions.

Answer: Arrow functions alleviate context-related bugs in nested functions by inheriting this from their enclosing lexical scope. This eliminates the need for workarounds such as storing this in a variable or using .bind() to maintain context.

Arrow functions contribute to avoiding context-related bugs in nested functions by capturing the value of this from their surrounding lexical scope. This behavior ensures that this inside the arrow function always refers to the same this value as the outer function, preventing unexpected changes in context.

Here’s an example illustrating how arrow functions help avoid context-related bugs in nested functions:

// Using regular functions
const obj = {
    name: 'Regular Object',
    regularFunction: function() {
        console.log('Inside regularFunction:', this.name);

        // Nested regular function
        setTimeout(function() {
            console.log('Inside nested regular function:', this.name); // 'this' refers to global object or 'undefined' (in strict mode)
        }, 1000);
    }
};

obj.regularFunction(); // Output: "Inside regularFunction: Regular Object"

// Using arrow functions
const arrowObj = {
    name: 'Arrow Object',
    arrowFunction: function() {
        console.log('Inside arrowFunction:', this.name);

        // Nested arrow function
        setTimeout(() => {
            console.log('Inside nested arrow function:', this.name); // 'this' refers to 'arrowObj'
        }, 1000);
    }
};

arrowObj.arrowFunction(); // Output: "Inside arrowFunction: Arrow Object"

In this example:

  • In the obj object, the regularFunction method uses a nested regular function inside a setTimeout. Inside the nested regular function, this does not refer to obj anymore due to the function context being changed by setTimeout. This can lead to bugs or unexpected behavior, such as this.name being undefined or referring to the global object.
  • In contrast, in the arrowObj object, the arrowFunction method uses a nested arrow function inside a setTimeout. Since arrow functions capture this from their surrounding lexical scope (arrowFunction in this case), this.name inside the arrow function still refers to the name property of arrowObj. This ensures that the context remains consistent and avoids context-related bugs.

5. In what scenarios would you prefer using arrow functions over regular functions?

Answer: Arrow functions are preferred in scenarios where you want to maintain the lexical scope of this, such as in callbacks, event handlers, or when defining methods in objects. Additionally, arrow functions provide a more concise syntax, making the code cleaner and more readable.

Callbacks: Arrow functions are often preferred for callbacks because they capture the surrounding lexical scope, which can be useful for maintaining the context of this.

const numbers = [1, 2, 3, 4, 5];

// Using a regular function for Array.prototype.map
const squares = numbers.map(function(num) {
    return num * num;
});

console.log(squares); // Output: [1, 4, 9, 16, 25]

// Using an arrow function for Array.prototype.map
const cubes = numbers.map(num => num * num * num);

console.log(cubes); // Output: [1, 8, 27, 64, 125]

Event Handlers: Arrow functions can be useful for event handlers because they maintain the lexical scope, making it easier to access properties of the surrounding object.

<button id="myButton">Click me</button>

<script>
const obj = {
    name: 'Arrow Object',
    handleClick: function() {
        console.log('Button clicked in', this.name);
    }
};

document.getElementById('myButton').addEventListener('click', obj.handleClick); // Output: "Button clicked in undefined"

// Using an arrow function to maintain context
document.getElementById('myButton').addEventListener('click', () => obj.handleClick()); // Output: "Button clicked in Arrow Object"
</script>

Defining Object Methods: Arrow functions can be concise when defining methods in objects and they maintain the lexical scope, making them suitable for accessing object properties.

const obj = {
    name: 'Arrow Object',
    // Using a regular function
    regularMethod: function() {
        console.log('Regular method called in', this.name);
    },
    // Using an arrow function
    arrowMethod: () => {
        console.log('Arrow method called in', this.name);
    }
};

obj.regularMethod(); // Output: "Regular method called in Arrow Object"
obj.arrowMethod(); // Output: "Arrow method called in undefined"

In these examples, arrow functions are preferred because they provide a concise syntax and maintain the lexical scope, which is beneficial for accessing this context and surrounding variables.

6. Explain what lexical scope is in JavaScript and provide an example of how it can be beneficial in a live project.

Lexical scope in JavaScript refers to how the scope of a variable is determined by its position in the code. It means that variables are accessible within the block they are defined in, as well as any nested blocks. This concept allows for predictable variable resolution and helps in organizing and encapsulating code.

Example

Consider a scenario where you’re building a web application with multiple modules. Each module may have its own set of variables and functions. Lexical scope helps in keeping these variables and functions scoped within their respective modules, preventing them from interfering with variables and functions in other modules.

// Module 1
function module1Function() {
    const module1Variable = 'Module 1 variable';
    console.log(module1Variable);
}

// Module 2
function module2Function() {
    const module2Variable = 'Module 2 variable';
    console.log(module2Variable);
}

// In the main script
module1Function(); // Output: "Module 1 variable"
module2Function(); // Output: "Module 2 variable"
// console.log(module1Variable); // This would result in an error since module1Variable is not defined in this scope

In this example, module1Variable and module2Variable are scoped within their respective modules due to lexical scope. This helps in keeping the variables encapsulated and prevents potential conflicts or unintended access from other parts of the codebase.

7. How can you create objects in JavaScript, and could you provide examples for each method?

In JavaScript, there are several ways to create objects. Here are some of the common ways along with examples:

Object Literal:

You can create an object using object literal notation, which is the simplest way to create an object.

const obj = { 
    key1: 'value1', 
    key2: 'value2' 
};

Constructor Function:

You can define a constructor function and create objects using the new keyword.

function Person(name, age) {
    this.name = name;
    this.age = age;
}

const person1 = new Person('Harry', 30);

Object.create():

You can create objects using Object.create() method, which allows you to specify the prototype of the newly-created object.

const personPrototype = {
    greeting: function() {
        return 'Hello, my name is ' + this.name;
    }
};

const person2 = Object.create(personPrototype);
person2.name = 'Harry';
person2.age = 25;

Class Syntax (ES6):

With ES6, you can use class syntax to define and create objects.

class Car {
    constructor(make, model) {
        this.make = make;
        this.model = model;
    }
}

const car1 = new Car('Toyota', 'Corolla');

Factory Function:

You can create objects using factory functions, which are functions that return objects.

function createPerson(name, age) {
    return {
        name: name,
        age: age,
        greeting: function() {
            return 'Hello, my name is ' + this.name;
        }
    };
}

const person3 = createPerson('Harry', 28);
console.log(person3.name); // Output: Harry
console.log(person3.age);  // Output: 28
console.log(person3.greeting()); // Output: Hello, my name is Harry

8. Can you explain the usage of the super keyword in JavaScript with an example?

In JavaScript, the super keyword is used to call functions or access properties on an object’s parent. It is commonly used in classes when working with inheritance to refer to the superclass’s methods or properties.

Here’s how super works with examples:

  1. Calling Superclass Constructor: In a subclass constructor, you can use super() to call the constructor of its superclass.
class Animal {
    constructor(name) {
        this.name = name;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Call the constructor of the superclass
        this.breed = breed;
    }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.name); // Output: Buddy
console.log(myDog.breed); // Output: Golden Retriever

2. Calling Superclass Methods: Inside methods of a subclass, you can use super.method() to call a method from its superclass with the same name.

class Animal {
    speak() {
        return 'Animal speaks';
    }
}

class Dog extends Animal {
    speak() {
        return super.speak() + ', but Dog barks'; // Call superclass method and add extra behavior
    }
}

const myDog = new Dog();
console.log(myDog.speak()); // Output: "Animal speaks, but Dog barks"

3. Accessing Superclass Properties: In a subclass, you can use super.property to access properties of its superclass.

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

class Dog extends Animal {
    constructor(name, breed) {
        super(name); // Call the constructor of the superclass
        this.breed = breed;
    }

    getDescription() {
        return super.name + ' is a ' + this.breed;
    }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.getDescription()); // Output: "Buddy is a Golden Retriever"

Leave a Comment