JavaScript Interview Q&A:

JavaScript Interview Q&A:

·

18 min read

From Basics to Advanced Concepts

In this blog post, we'll dive into some essential JavaScript concepts that are frequently asked about in interviews. We'll explore these topics through a Q&A format between an interviewer and a candidate, providing detailed explanations, examples, use cases, pros, and cons for each concept.

1. Let, Const, and Var

Interviewer: Can you explain the differences between let, const, and var in JavaScript?

Candidate: Certainly! let, const, and var are all used for variable declarations in JavaScript, but they have some key differences:

  1. var:

    • Function-scoped or globally-scoped

    • Can be redeclared and updated

    • Hoisted to the top of its scope and initialized with undefined

  2. let:

    • Block-scoped

    • Can be updated but not redeclared in the same scope

    • Not initialized until its declaration is evaluated (temporal dead zone)

  3. const:

    • Block-scoped

    • Cannot be updated or redeclared

    • Must be initialized at declaration

    • For objects and arrays, the reference is constant, but properties can be modified

Here's an example to illustrate:

function exampleScope() {
  var x = 1;
  let y = 2;
  const z = 3;

  if (true) {
    var x = 4; // Same 'x' as outer scope
    let y = 5; // Different 'y', block-scoped
    const z = 6; // Different 'z', block-scoped
    console.log(x, y, z); // Output: 4 5 6
  }

  console.log(x, y, z); // Output: 4 2 3
}

exampleScope();

Use cases:

  • Use const by default for variables that won't be reassigned

  • Use let for variables that will be reassigned

  • Avoid var in modern JavaScript due to its function scope and hoisting behavior

Pros and cons:

  • let and const provide better scoping rules and help prevent unintended variable mutations

  • const helps in writing more predictable code

  • var can lead to unexpected behavior due to hoisting and function scope

2. Arrow Functions vs Normal Functions

Interviewer: What are the main differences between arrow functions and normal functions in JavaScript?

Candidate: Arrow functions and normal functions have several key differences:

  1. Syntax:

    • Normal function: function name(params) { ... }

    • Arrow function: (params) => { ... } or param => expression

  2. this binding:

    • Normal functions create their own this context

    • Arrow functions inherit this from the enclosing scope

  3. Arguments object:

    • Normal functions have an arguments object

    • Arrow functions don't have their own arguments object

  4. Implicit return:

    • Arrow functions can have an implicit return for single expressions
  5. Use as methods:

    • Normal functions work well as object methods

    • Arrow functions are not suitable for methods that need to access object properties using this

Here's an example demonstrating these differences:

const obj = {
  name: "John",
  normalFunc: function() {
    console.log(this.name);
    console.log(arguments);
  },
  arrowFunc: () => {
    console.log(this.name);
    // console.log(arguments); // This would cause an error
  },
  implicitReturn: x => x * 2
};

obj.normalFunc("arg1", "arg2"); // Output: "John" and arguments object
obj.arrowFunc("arg1", "arg2"); // Output: undefined (or global object's name property)
console.log(obj.implicitReturn(4)); // Output: 8

Use cases:

  • Use arrow functions for short, simple functions and callbacks

  • Use normal functions for methods that need to access this or arguments

  • Use arrow functions for lexical this binding in nested functions

Pros and cons:

  • Arrow functions provide a more concise syntax

  • Arrow functions help avoid this binding issues in callbacks

  • Normal functions are more versatile and work better as object methods

3. 'this' Keyword in Normal Functions vs Arrow Functions

Interviewer: How does the behavior of the this keyword differ in normal functions compared to arrow functions?

Candidate: The behavior of this is one of the key differences between normal functions and arrow functions:

  1. Normal Functions:

    • this is dynamically bound based on how the function is called

    • The value of this can change depending on the invocation context

    • Can be explicitly bound using call(), apply(), or bind()

  2. Arrow Functions:

    • this is lexically bound (captured from the surrounding scope)

    • this retains the value of the enclosing lexical context

    • Cannot be rebound using call(), apply(), or bind()

Let's look at an example to illustrate this difference:

const obj = {
  name: "Alice",
  normalFunc: function() {
    console.log("Normal function:", this.name);
    setTimeout(function() {
      console.log("Normal function timeout:", this.name);
    }, 100);
  },
  arrowFunc: function() {
    console.log("Arrow function:", this.name);
    setTimeout(() => {
      console.log("Arrow function timeout:", this.name);
    }, 100);
  }
};

obj.normalFunc();
// Output:
// Normal function: Alice
// Normal function timeout: undefined (or global object's name property)

obj.arrowFunc();
// Output:
// Arrow function: Alice
// Arrow function timeout: Alice

In this example, the normal function loses its this context in the setTimeout callback, while the arrow function retains it.

Use cases:

  • Use normal functions when you need dynamic this binding (e.g., object methods, constructors)

  • Use arrow functions in callbacks to preserve the lexical this

  • Use arrow functions in class fields for auto-bound methods

Pros and cons:

  • Arrow functions help avoid common pitfalls with this in callbacks

  • Normal functions offer more flexibility in this binding

  • Arrow functions can't be used as constructors or when you need to rebind this

4. Scope

Interviewer: Can you explain the concept of scope in JavaScript and the different types of scope?

Candidate: Certainly! Scope in JavaScript refers to the current context of code, which determines the accessibility of variables to JavaScript. There are three main types of scope:

  1. Global Scope:

    • Variables declared outside any function or block

    • Accessible from anywhere in the program

  2. Function Scope:

    • Variables declared within a function

    • Accessible only within that function

  3. Block Scope (introduced with ES6):

    • Variables declared within a block (e.g., if statements, loops)

    • Only applies to variables declared with let and const

Here's an example demonstrating these scopes:

// Global scope
var globalVar = "I'm global";
let globalLet = "I'm also global";

function exampleFunction() {
  // Function scope
  var functionVar = "I'm function-scoped";
  let functionLet = "I'm also function-scoped";

  if (true) {
    // Block scope
    var blockVar = "I'm function-scoped (var ignores blocks)";
    let blockLet = "I'm block-scoped";
    const blockConst = "I'm also block-scoped";

    console.log(globalVar); // Accessible
    console.log(functionVar); // Accessible
    console.log(blockLet); // Accessible
  }

  console.log(blockVar); // Accessible (function-scoped)
  // console.log(blockLet); // Error: not defined
}

exampleFunction();
console.log(globalVar); // Accessible
// console.log(functionVar); // Error: not defined

Use cases:

  • Use block scope (let and const) to limit variable accessibility and prevent unintended modifications

  • Use function scope to encapsulate variables within functions

  • Minimize use of global scope to avoid naming conflicts and improve code organization

Pros and cons:

  • Block scope helps prevent variable leaking and improves code clarity

  • Function scope allows for data privacy and encapsulation

  • Global scope can lead to naming conflicts and make code harder to maintain

5. Closure

Interviewer: What is a closure in JavaScript, and how does it work?

Candidate: A closure is a fundamental concept in JavaScript. It's a function that has access to variables in its outer (enclosing) lexical scope, even after the outer function has returned. In other words, a closure "closes over" the variables from its outer scope.

Key points about closures:

  1. They allow for data privacy and encapsulation

  2. They can be used to create factory functions

  3. They're the basis for many JavaScript patterns and techniques

Here's an example of a closure:

function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter1 = createCounter();
const counter2 = createCounter();

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (separate instance)

In this example, the inner function forms a closure over the count variable. Each call to createCounter() creates a new closure with its own count.

Use cases:

  1. Data privacy: Create private variables and methods

  2. Function factories: Generate functions with customized behavior

  3. Memoization: Cache expensive function calls

  4. Callbacks and event handlers: Preserve scope in asynchronous code

Here's an example of using a closure for data privacy:

function createBank() {
  let balance = 0;
  return {
    deposit: function(amount) {
      balance += amount;
      return balance;
    },
    withdraw: function(amount) {
      if (amount > balance) {
        return "Insufficient funds";
      }
      balance -= amount;
      return balance;
    }
  };
}

const myAccount = createBank();
console.log(myAccount.deposit(100)); // 100
console.log(myAccount.withdraw(30)); // 70
console.log(myAccount.balance); // undefined (private variable)

Pros and cons: Pros:

  • Enables data privacy and encapsulation

  • Allows for the creation of function factories

  • Preserves state between function calls

Cons:

  • Can lead to memory leaks if not managed properly

  • May be confusing for developers not familiar with the concept

  • Overuse can make code harder to understand and maintain

6. Hoisting

Interviewer: Can you explain the concept of hoisting in JavaScript?

Candidate: Certainly! Hoisting is a behavior in JavaScript where variable and function declarations are moved to the top of their respective scopes during the compilation phase, before the code is executed. It's important to note that only the declarations are hoisted, not the initializations.

Key points about hoisting:

  1. Function declarations are fully hoisted (both declaration and definition)

  2. var variables are hoisted and initialized with undefined

  3. let and const declarations are hoisted but not initialized (temporal dead zone)

  4. Function expressions are not hoisted

Here's an example demonstrating hoisting:

console.log(x); // undefined (not a ReferenceError)
var x = 5;

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;

hoistedFunction(); // "I'm hoisted!"
function hoistedFunction() {
  console.log("I'm hoisted!");
}

notHoisted(); // TypeError: notHoisted is not a function
var notHoisted = function() {
  console.log("I'm not hoisted!");
};

The above code is interpreted by JavaScript as:

function hoistedFunction() {
  console.log("I'm hoisted!");
}

var x;
var notHoisted;

console.log(x);
x = 5;

console.log(y); // Still in the temporal dead zone
let y = 10;

hoistedFunction();

notHoisted();
notHoisted = function() {
  console.log("I'm not hoisted!");
};

Use cases:

  • Understanding hoisting is crucial for debugging and avoiding unexpected behavior

  • Hoisting allows you to use functions before their actual declaration in the code

Pros and cons: Pros:

  • Allows for more flexible code structure (e.g., calling functions before their declaration)

  • Helps understand JavaScript's execution context and scope

Cons:

  • Can lead to confusion and unexpected behavior if not well understood

  • var hoisting can cause issues with variable scoping

  • Relying on hoisting can make code less readable and harder to maintain

Best practice is to declare variables at the top of their scope and functions before they are used to avoid confusion caused by hoisting.

7. Template Literals

Interviewer: What are template literals in JavaScript, and how do they differ from traditional string concatenation?

Candidate: Template literals, introduced in ES6 (ECMAScript 2015), are a way to create strings that allows for more flexible and readable string interpolation. They are denoted by backticks (`) instead of single or double quotes.

Key features of template literals:

  1. String interpolation using ${} syntax

  2. Multi-line strings without explicit line breaks

  3. Tagged templates for custom string parsing

Here's an example comparing template literals with traditional string concatenation:

// Traditional string concatenation
const name = "Alice";
const age = 30;
const traditionalString = "My name is " + name + " and I am " + age + " years old.";

// Template literal
const templateLiteral = `My name is ${name} and I am ${age} years old.`;

console.log(traditionalString === templateLiteral); // true

// Multi-line string
const multiLine = `
  This is a
  multi-line
  string.
`;

console.log(multiLine);

Use cases:

  1. Creating complex strings with variables

  2. Generating HTML templates

  3. Multi-line strings

  4. Tagged templates for custom string processing

Here's an example of a more complex use case with HTML generation:

function createUserCard(user) {
  return `
    <div class="user-card">
      <img src="${user.avatar}" alt="${user.name}'s avatar">
      <h2>${user.name}</h2>
      <p>Age: ${user.age}</p>
      <p>Email: ${user.email}</p>
    </div>
  `;
}

const user = {
  name: "Alice",
  age: 30,
  email: "alice@example.com",
  avatar: "https://example.com/avatar.jpg"
};

document.body.innerHTML = createUserCard(user);

Pros and cons: Pros:

  • More readable and maintainable string interpolation

  • Easy creation of multi-line strings

  • Allows for powerful string manipulation with tagged templates

Cons:

  • Not supported in older browsers (requires transpilation for full compatibility)

  • Can be misused to create overly complex inline expressions

8. Default Parameters

Interviewer: Can you explain what default parameters are in JavaScript and how they work?

Candidate: Certainly! Default parameters in JavaScript allow us to specify default values for function parameters. If an argument is not provided or is undefined when the function is called, the default value will be used instead.

Here's a simple example:

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

greet(); // Output: Hello, Guest!
greet("Alice"); // Output: Hello, Alice!

In this example, if no argument is passed to the greet function, it uses "Guest" as the default value for the name parameter.

Use cases:

  1. Providing fallback values for optional parameters

  2. Simplifying function calls by reducing the need for parameter checks

  3. Making APIs more flexible and easier to use

Pros:

  • Cleaner and more readable code

  • Reduces boilerplate for parameter validation

  • Helps prevent errors from undefined parameters

Cons:

  • Only available in ES6 and later, so it might not work in older environments

  • Can make it less obvious what values a function might use if not carefully documented

Interviewer: That's a good explanation. Can you give an example of a more complex use case for default parameters?

Candidate: Certainly! Let's consider a function that creates a configuration object for a web application:

function createConfig(
  env = "development",
  database = { host: "localhost", port: 5432 },
  features = ["auth", "api"]
) {
  return {
    environment: env,
    db: database,
    enabledFeatures: features,
    isProduction: env === "production"
  };
}

// Using default values
console.log(createConfig());
// Output: { environment: "development", db: { host: "localhost", port: 5432 }, enabledFeatures: ["auth", "api"], isProduction: false }

// Overriding some defaults
console.log(createConfig("production", { host: "db.example.com", port: 5432 }));
// Output: { environment: "production", db: { host: "db.example.com", port: 5432 }, enabledFeatures: ["auth", "api"], isProduction: true }

In this example, we have default values for the environment, database configuration, and enabled features. This allows users of the createConfig function to easily create configurations with sensible defaults while still having the flexibility to override any or all of the parameters as needed.

9. Ternary Operator

Interviewer: What is the ternary operator in JavaScript, and when would you use it?

Candidate: The ternary operator in JavaScript is a concise way to write an if-else statement in a single line. It's often used for simple conditional expressions. The syntax is:

condition ? expressionIfTrue : expressionIfFalse

Here's a simple example:

const age = 20;
const canVote = age >= 18 ? "Yes" : "No";
console.log(canVote); // Output: Yes

In this case, if age is 18 or greater, canVote will be "Yes"; otherwise, it will be "No".

Use cases:

  1. Assigning a value to a variable based on a condition

  2. Returning different values from a function based on a condition

  3. Conditional rendering in JSX (React)

Pros:

  • Concise and readable for simple conditions

  • Can make code more compact

  • Useful for inline conditional expressions

Cons:

  • Can become hard to read if nested or used with complex conditions

  • Might be confusing for beginners

  • Overuse can lead to less maintainable code

Interviewer: Can you show an example of when not to use the ternary operator?

Candidate: Absolutely. While the ternary operator is useful, it can become difficult to read when used with complex conditions or nested. Here's an example of when not to use it:

// Bad usage of ternary operator
const result = condition1 ? value1
             : condition2 ? value2
             : condition3 ? value3
             : defaultValue;

// Better as an if-else statement
let result;
if (condition1) {
  result = value1;
} else if (condition2) {
  result = value2;
} else if (condition3) {
  result = value3;
} else {
  result = defaultValue;
}

In this case, the nested ternary operators make the code hard to read and understand at a glance. Using a standard if-else structure is clearer and more maintainable.

10. ES5 vs ES6

Interviewer: Can you explain some key differences between ES5 and ES6 (ES2015)?

Candidate: Certainly! ES6, also known as ECMAScript 2015, introduced several new features and improvements to JavaScript. Here are some key differences:

  1. Let and Const: ES6 introduced let and const for block-scoped variable declarations, whereas ES5 only had var.

     // ES5
     var x = 5;
    
     // ES6
     let y = 5;
     const Z = 5;
    
  2. Arrow Functions: ES6 introduced a more concise syntax for function expressions.

     // ES5
     var add = function(a, b) {
       return a + b;
     };
    
     // ES6
     const add = (a, b) => a + b;
    
  3. Template Literals: ES6 added support for template literals, allowing for easier string interpolation.

     // ES5
     var name = "Alice";
     var greeting = "Hello, " + name + "!";
    
     // ES6
     const name = "Alice";
     const greeting = `Hello, ${name}!`;
    
  4. Destructuring: ES6 introduced destructuring for arrays and objects.

     // ES6
     const [a, b] = [1, 2];
     const { x, y } = { x: 3, y: 4 };
    
  5. Default Parameters: As we discussed earlier, ES6 added support for default function parameters.

  6. Classes: ES6 introduced a more intuitive syntax for creating classes and dealing with inheritance.

     // ES6
     class Animal {
       constructor(name) {
         this.name = name;
       }
       speak() {
         console.log(`${this.name} makes a sound.`);
       }
     }
    
  7. Modules: ES6 introduced a standardized module syntax using import and export.

     // ES6
     import { func } from './module';
     export const variable = 5;
    

Pros of ES6:

  • More expressive and concise syntax

  • Better support for functional programming concepts

  • Improved handling of asynchronous operations with Promises and async/await

  • Enhanced support for object-oriented programming

Cons:

  • Not supported in older browsers without transpilation

  • Learning curve for developers used to ES5

  • Some features (like modules) require additional setup in many environments

11. Inheritance in JavaScript (Prototype)

Interviewer: How does inheritance work in JavaScript, particularly with regards to prototypes?

Candidate: In JavaScript, inheritance is primarily achieved through prototypes. Every object in JavaScript has an internal property called [[Prototype]] (accessed via __proto__ or Object.getPrototypeOf()), which refers to another object. This creates a prototype chain.

Here's how it works:

  1. When you try to access a property on an object, JavaScript first looks for the property on the object itself.

  2. If it doesn't find it, it looks on the object's prototype.

  3. If it's not there, it looks on the prototype's prototype, and so on up the chain.

  4. This continues until it either finds the property or reaches an object with a null prototype (usually Object.prototype).

Here's an example of how we can use prototypes for inheritance:

// Constructor function
function Animal(name) {
  this.name = name;
}

// Adding a method to the prototype
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound.`);
};

// Creating a new object
const dog = new Animal("Rex");
dog.speak(); // Output: Rex makes a sound.

// Inheritance
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

// Set up prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add a method specific to Dog
Dog.prototype.bark = function() {
  console.log("Woof!");
};

const myDog = new Dog("Buddy", "Labrador");
myDog.speak(); // Output: Buddy makes a sound.
myDog.bark(); // Output: Woof!

Use cases:

  1. Creating object hierarchies

  2. Sharing methods across multiple objects

  3. Implementing polymorphism

Pros:

  • Memory efficient (shared methods)

  • Flexible and powerful

  • Allows for dynamic runtime changes

Cons:

  • Can be confusing for developers coming from class-based languages

  • Prototype pollution can be a security risk if not handled carefully

  • Performance can degrade with long prototype chains

Interviewer: That's a good explanation. Can you briefly describe how ES6 classes relate to this prototype-based inheritance?

Candidate: Certainly! ES6 classes are essentially syntactic sugar over the prototype-based inheritance we just discussed. They provide a more intuitive and cleaner syntax for creating objects and implementing inheritance, but under the hood, they still use prototypes.

Here's the previous example rewritten using ES6 classes:

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

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log("Woof!");
  }
}

const myDog = new Dog("Buddy", "Labrador");
myDog.speak(); // Output: Buddy makes a sound.
myDog.bark(); // Output: Woof!

This code is functionally equivalent to the prototype-based example, but it's more readable and familiar to developers coming from class-based languages. However, it's important to remember that JavaScript remains a prototype-based language, and understanding prototypes is still crucial for advanced JavaScript development.

13. Temporal Dead Zone

Interviewer: Can you explain what the Temporal Dead Zone is in JavaScript?

Candidate: The Temporal Dead Zone (TDZ) is a behavior in JavaScript that occurs with variables declared using let and const. It refers to the period between entering a scope and the point where the variable is declared and initialized.

During this period, if you try to access the variable, you'll get a ReferenceError, even though the variable declaration exists in the scope.

Here's an example to illustrate:

console.log(x); // Outputs: undefined
var x = 5;

console.log(y); // Throws ReferenceError: Cannot access 'y' before initialization
let y = 10;

In this example, x is hoisted and initialized with undefined, so logging it before the declaration doesn't throw an error. However, y is in the TDZ until the point of its declaration, so trying to access it throws an error.

The TDZ also applies to const declarations and class declarations.

Use cases: The TDZ isn't something you "use" per se, but understanding it is crucial for:

  1. Debugging unexpected ReferenceErrors

  2. Writing cleaner code by declaring variables before use

  3. Understanding the differences between var, let, and const

Pros:

  • Helps catch errors where variables are used before they're declared

  • Enforces cleaner coding practices

  • Makes const behave more consistently (always initialized at declaration)

Cons:

  • Can be confusing for developers new to ES6+

  • Might lead to unexpected errors if not well understood

Interviewer: That's a good explanation. Can you give an example of how the TDZ might affect function declarations?

Candidate: Certainly! The Temporal Dead Zone also affects function parameters that use default values referencing other parameters. Here's an example:

function example(x = y, y = 1) {
  console.log(x, y);
}

example(); // Throws ReferenceError: Cannot access 'y' before initialization
example(undefined, 2); // Outputs: 2 2

In the first call to example(), we get a ReferenceError because x is trying to use y as its default value, but y is still in the TDZ at that point.

In the second call, we explicitly pass undefined for x, which triggers the default parameter, but now y is already initialized with 2, so x takes on that value.

This demonstrates why it's generally a good practice to list parameters with default values after parameters without default values:

function betterExample(y = 1, x = y) {
  console.log(x, y);
}

betterExample(); // Outputs: 1 1

Understanding these nuances of the TDZ can help prevent subtle bugs in function declarations and calls.