Introduction

Welcome to the JavaScript Mastery: Comprehensive MCQ Challenge! This chapter is meticulously designed to test and solidify your understanding of JavaScript’s most intricate and often counter-intuitive behaviors. Far beyond basic syntax, this challenge delves into the “weird parts” of JavaScript that often trip up even experienced developers.

Whether you’re aiming for a mid-level frontend role or an architect position, mastering these concepts—including coercion, hoisting, scope, closures, prototypes, this binding, the event loop, asynchronous patterns, and memory management—is crucial. Interviewers at top companies frequently use these topics to gauge a candidate’s deep understanding of the language’s internals and their ability to debug complex, real-world scenarios.

This comprehensive Multiple Choice Question (MCQ) section is tailored to prepare you for the trickiest JavaScript questions you might encounter. Each question is crafted to highlight a specific nuance or edge case, providing detailed explanations to ensure you not only know the answer but also understand the underlying “why” in accordance with modern JavaScript standards (ECMAScript 2025/2026).

MCQ Section: JavaScript’s Weird Parts

Test your knowledge with these challenging questions covering advanced JavaScript concepts.


Question 1: Hoisting and Function Declarations

What will be the output of the following JavaScript code?

var x = 1;
function foo() {
  x = 10;
  return;
  function x() {}
}
foo();
console.log(x);

A. 1 B. 10 C. undefined D. ReferenceError

Correct Answer: A

Explanation: This question tests your understanding of hoisting and function-scoped variable declarations.

  1. Global Scope: var x = 1; declares a global variable x and initializes it to 1.
  2. foo Function Scope: Inside foo, there’s a function declaration function x() {}. Due to hoisting, this function declaration is hoisted to the top of foo’s scope. This creates a local variable x (which is a function) within foo’s scope, effectively shadowing the global x.
  3. Assignment within foo: The line x = 10; then attempts to assign 10 to x. Since the local x (the hoisted function) exists, this assignment modifies the local function x to become the number 10.
  4. return statement: The return statement immediately exits the foo function.
  5. console.log(x): After foo() executes, the console.log(x) statement accesses the global x. Since the assignment x = 10 modified the local x inside foo and not the global x, the global x remains 1.

Key Points:

  • Function declarations are hoisted before variable declarations within their respective scopes.
  • A hoisted function declaration creates a local variable that can shadow a global one.
  • Assignments inside a function will affect the local variable if one exists, not the global one, unless explicitly referenced (e.g., window.x).

Common Mistakes:

  • Assuming x = 10 modifies the global x.
  • Forgetting that function declarations are hoisted.
  • Not understanding how local scope shadows global scope.

Follow-up: What if var x = 1; was let x = 1; and function x() {} was removed, but let x = 10; was added inside foo? How would console.log(x) behave?


Question 2: Type Coercion and Equality

What is the result of [] == ![] in JavaScript?

A. true B. false C. TypeError D. NaN

Correct Answer: A

Explanation: This is a classic example of JavaScript’s abstract equality (==) operator and type coercion rules.

  1. Right-hand side (![]):
    • The ! (logical NOT) operator first tries to convert its operand to a boolean.
    • An empty array [] is a “truthy” value in JavaScript.
    • So, ![] evaluates to !true, which is false.
  2. Left-hand side ([]):
    • Now we have [] == false.
    • According to the ECMAScript specification for == when comparing an object (like []) with a boolean (false):
      • The boolean false is first converted to a number: false becomes 0.
      • So, the comparison becomes [] == 0.
      • Next, the object [] is converted to a primitive value. For arrays, this involves calling [].valueOf() (which returns []) and then [].toString() (which returns an empty string "").
      • So, the comparison becomes "" == 0.
      • Finally, the empty string "" is converted to a number: "" becomes 0.
      • So, the comparison becomes 0 == 0.
    • 0 == 0 is true.

Key Points:

  • ![] evaluates to false because [] is truthy.
  • When comparing an object with a boolean using ==, the boolean is converted to a number (0 or 1).
  • The object is then converted to a primitive (usually via toString()) and then potentially to a number for comparison.

Common Mistakes:

  • Assuming == behaves like === (strict equality).
  • Incorrectly converting [] to false or 0 prematurely.
  • Not knowing the order of operations for type coercion.

Follow-up: Explain the difference between == and ===. Provide an example where 0 == null is false but 0 == undefined is also false, yet null == undefined is true.


Question 3: Closures and Loop Variables

Consider the following code:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 100);
}

What will be logged to the console?

A. 0, 1, 2 (after 100ms each) B. 3, 3, 3 (after 100ms) C. 0, 1, 2 (immediately) D. undefined, undefined, undefined

Correct Answer: B

Explanation: This is a classic closure problem related to var in loops and asynchronous operations.

  1. var and Function Scope: The var i declaration is function-scoped (or global-scoped if outside a function). In a for loop, var does not create a new i for each iteration. Instead, there’s only one i variable shared across all iterations.
  2. Asynchronous setTimeout: The setTimeout function schedules its callback to run after the current execution stack has cleared and a minimum delay has passed. The loop completes its execution almost instantly.
  3. Loop Completion: By the time the setTimeout callbacks actually execute (after approximately 100ms), the for loop has already finished. The value of i has incremented to 3 (because i < 3 is no longer true, so i++ made it 3).
  4. Closure: Each setTimeout callback forms a closure over the outer scope where i is defined. When the callbacks finally execute, they all refer to the same, single i variable, which now holds the value 3.

Key Points:

  • var is function-scoped, not block-scoped.
  • setTimeout callbacks are executed asynchronously, after the main thread finishes.
  • Closures capture variables by reference, not by value, from their outer lexical environment.

Common Mistakes:

  • Assuming i is block-scoped like let or const.
  • Not understanding the asynchronous nature of setTimeout.

Follow-up: How would you fix this code to log 0, 1, 2? Provide at least two different modern JavaScript solutions.


Question 4: this Binding in Arrow Functions

What will be logged to the console when obj.greet() is called?

const obj = {
  name: 'Alice',
  greet: function() {
    setTimeout(() => {
      console.log(`Hello, my name is ${this.name}`);
    }, 0);
  }
};
obj.greet();

A. Hello, my name is Alice B. Hello, my name is undefined C. Hello, my name is (empty string) D. TypeError: Cannot read properties of undefined (reading 'name')

Correct Answer: A

Explanation: This question highlights a key characteristic of arrow functions regarding this binding.

  1. obj.greet() call: When obj.greet() is called, the greet method is invoked as a method of obj. Therefore, inside the greet function, this refers to obj.
  2. setTimeout callback (Arrow Function): The callback passed to setTimeout is an arrow function (() => { ... }). Arrow functions do not have their own this binding. Instead, they lexically inherit this from their enclosing scope.
  3. Lexical this: In this case, the arrow function’s enclosing scope is the greet method. Since this inside greet is obj, the arrow function’s this will also be obj.
  4. Accessing name: Thus, this.name inside the arrow function correctly refers to obj.name, which is 'Alice'.

Key Points:

  • Regular functions (function() {}) define their own this based on how they are called.
  • Arrow functions (() => {}) do not define their own this; they inherit this from their immediate lexical (enclosing) scope.
  • setTimeout itself does not change the this context for the callback; the this behavior depends solely on whether the callback is a regular function or an arrow function.

Common Mistakes:

  • Assuming setTimeout always binds this to the global object (window in browsers, undefined in strict mode/modules). This is true for regular functions passed to setTimeout without explicit binding, but not for arrow functions.
  • Confusing this binding rules for regular functions with arrow functions.

Follow-up: How would the output change if the setTimeout callback was a regular function (function() { ... }) instead of an arrow function? How could you then ensure this.name still refers to 'Alice'?


Question 5: Event Loop and Microtasks vs. Macrotasks

What is the order of outputs for the following code?

console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
});

setTimeout(() => {
  console.log('Timeout 2');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 2');
});

console.log('End');

A. Start, End, Promise 1, Promise 2, Timeout 1, Timeout 2 B. Start, Promise 1, Promise 2, End, Timeout 1, Timeout 2 C. Start, End, Timeout 1, Promise 1, Timeout 2, Promise 2 D. Start, Promise 1, Timeout 1, End, Promise 2, Timeout 2

Correct Answer: A

Explanation: This question tests your understanding of the JavaScript event loop, specifically the distinction between macrotasks and microtasks.

  1. Main Thread/Call Stack:

    • console.log('Start') executes immediately.
    • setTimeout(() => { console.log('Timeout 1'); }, 0) schedules a macrotask.
    • Promise.resolve().then(() => { console.log('Promise 1'); }) schedules a microtask.
    • setTimeout(() => { console.log('Timeout 2'); }, 0) schedules another macrotask.
    • Promise.resolve().then(() => { console.log('Promise 2'); }) schedules another microtask.
    • console.log('End') executes immediately.
    • The call stack is now empty.
  2. Event Loop Cycle:

    • The event loop first checks the microtask queue. It processes all microtasks before moving to the next macrotask.
      • Promise 1 is logged.
      • Promise 2 is logged.
    • After the microtask queue is empty, the event loop takes the next macrotask from the macrotask queue.
      • Timeout 1 is logged.
    • The event loop checks for microtasks (none). Then it takes the next macrotask.
      • Timeout 2 is logged.

Key Points:

  • Macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering.
  • Microtasks: Promise.then(), queueMicrotask(), MutationObserver.
  • The event loop prioritizes microtasks. All microtasks in the queue are processed after the current macrotask completes and before the next macrotask is picked up.

Common Mistakes:

  • Assuming setTimeout(..., 0) means it runs immediately after the current script.
  • Not understanding that microtasks have higher priority than macrotasks.
  • Incorrectly ordering multiple promises or multiple timeouts.

Follow-up: If queueMicrotask(() => console.log('Microtask')) was added before the first setTimeout, where would its output appear?


Question 6: Prototype Chain and Property Access

What will be the output of the following code?

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

Animal.prototype.speak = function() {
  return `${this.name} makes a sound.`;
};

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

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.speak = function() {
  return `${this.name} barks.`;
};

const myDog = new Dog('Buddy', 'Golden Retriever');
const genericAnimal = new Animal('Leo');

console.log(myDog.speak());
console.log(genericAnimal.speak());

A. Buddy barks., Leo makes a sound. B. Buddy makes a sound., Leo makes a sound. C. Buddy barks., Leo barks. D. TypeError: myDog.speak is not a function, Leo makes a sound.

Correct Answer: A

Explanation: This question tests your understanding of classical inheritance patterns in JavaScript using prototypes.

  1. Animal Constructor and Prototype: Animal is a constructor, and its speak method is on Animal.prototype.
  2. Dog Constructor: Dog calls Animal.call(this, name) to inherit properties from Animal.
  3. Dog Prototype Chain Setup:
    • Dog.prototype = Object.create(Animal.prototype): This line correctly sets up the prototype chain. Dog.prototype now has Animal.prototype as its __proto__. This means if a property isn’t found on Dog.prototype, JavaScript will look for it on Animal.prototype.
    • Dog.prototype.constructor = Dog: This correctly resets the constructor property, which is good practice.
  4. Dog.prototype.speak Override: Dog.prototype.speak = function() { ... } overrides the speak method that would otherwise be inherited from Animal.prototype. When myDog.speak() is called, JavaScript finds the speak method directly on Dog.prototype first.
  5. myDog.speak(): myDog is an instance of Dog. When myDog.speak() is called, the JavaScript engine looks for speak on myDog’s own properties, then on Dog.prototype. It finds the overridden speak method on Dog.prototype, which logs Buddy barks..
  6. genericAnimal.speak(): genericAnimal is an instance of Animal. When genericAnimal.speak() is called, the engine looks for speak on genericAnimal’s own properties, then on Animal.prototype. It finds Animal.prototype.speak, which logs Leo makes a sound..

Key Points:

  • Object.create() is the standard way to set up prototypal inheritance in ES5+.
  • Methods defined directly on a child’s prototype (Dog.prototype.speak) take precedence over methods higher up the prototype chain (Animal.prototype.speak).
  • this inside a prototype method refers to the instance on which the method was called.

Common Mistakes:

  • Using Dog.prototype = new Animal(); which incorrectly creates an instance of Animal and can lead to issues with shared properties.
  • Forgetting to reset Dog.prototype.constructor.
  • Incorrectly predicting which speak method will be called.

Follow-up: How would you modify the Dog.prototype.speak method to call the Animal.prototype.speak method and then add its own specific dog sound?


Question 7: Scoping with var, let, and const

What will be the output of the following code snippet?

(function() {
  var a = 1;
  let b = 2;
  const c = 3;

  if (true) {
    var a = 4;
    let b = 5;
    const c = 6;
    console.log(a);
    console.log(b);
    console.log(c);
  }
  console.log(a);
  console.log(b);
  console.log(c);
})();

A. 4, 5, 6, 4, 2, 3 B. 4, 5, 6, 1, 2, 3 C. 4, 5, 6, 4, 5, 6 D. 4, 5, 6, 1, 5, 6

Correct Answer: A

Explanation: This question tests your understanding of var (function-scoped) vs. let and const (block-scoped).

  1. Outer Scope (IIFE):

    • var a = 1;
    • let b = 2;
    • const c = 3; These create variables in the immediate function scope of the IIFE.
  2. Inner if Block Scope:

    • var a = 4;: This re-declares the same a variable from the outer function scope, because var is function-scoped. It effectively reassigns the value of a to 4.
    • let b = 5;: This declares a new, block-scoped variable b specific to the if block. It shadows the outer b.
    • const c = 6;: This declares a new, block-scoped variable c specific to the if block. It shadows the outer c.
  3. Inside if block console.logs:

    • console.log(a); outputs 4 (the reassigned function-scoped a).
    • console.log(b); outputs 5 (the block-scoped b).
    • console.log(c); outputs 6 (the block-scoped c).
  4. Outside if block console.logs:

    • console.log(a); outputs 4 (the function-scoped a was reassigned to 4 inside the if block, and this change persists).
    • console.log(b); outputs 2 (the block-scoped b=5 is out of scope, so the outer b=2 is accessed).
    • console.log(c); outputs 3 (the block-scoped c=6 is out of scope, so the outer c=3 is accessed).

Key Points:

  • var declarations are hoisted and function-scoped. Re-declaring a var variable within the same function scope (even in nested blocks) effectively reassigns the existing variable.
  • let and const declarations are block-scoped. Declaring let or const variables within a nested block creates new variables that only exist within that block, shadowing any variables of the same name in outer scopes.

Common Mistakes:

  • Treating var as block-scoped.
  • Assuming let and const reassign outer variables instead of creating new ones.

Follow-up: What would happen if the second var a = 4; was changed to let a = 4;?


Question 8: Memory Management and Closures

Consider the following code snippet:

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

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

counter1();
counter1();
counter2();

What will be the output, and how does memory management relate to count in this scenario?

A. 1, 2, 1. count is garbage collected after createCounter finishes each time. B. 1, 2, 1. Each counter function maintains its own independent count variable due to closures. C. 1, 2, 3. count is a shared global variable. D. 1, 1, 1. count is reset on each call to the inner function.

Correct Answer: B

Explanation: This question demonstrates a fundamental use case of closures and their implications for memory.

  1. createCounter Execution: When createCounter() is called, a new execution context is created. Inside this context, a new let count = 0; variable is initialized.
  2. Returning Inner Function: createCounter then returns an inner function. This inner function “closes over” (forms a closure with) the lexical environment in which it was created. This environment includes the count variable.
  3. counter1 and counter2:
    • const counter1 = createCounter(); creates the first closure. This counter1 function now has its own private count variable (initialized to 0) that it can access and modify.
    • const counter2 = createCounter(); creates a second, independent closure. This counter2 function has its own separate count variable (also initialized to 0) that it can access and modify.
  4. Execution:
    • counter1(): count (for counter1) becomes 1. Logs 1.
    • counter1(): count (for counter1) becomes 2. Logs 2.
    • counter2(): count (for counter2) becomes 1. Logs 1.

Memory Management:

  • The count variable for counter1 (and similarly for counter2) is not garbage collected when createCounter finishes because it is still being referenced by the returned inner function (counter1 itself). As long as counter1 (or counter2) exists and is accessible, its associated count variable will remain in memory.
  • Each count variable is distinct and managed independently for each closure created.

Key Points:

  • A closure allows an inner function to access variables from its outer (enclosing) function’s scope, even after the outer function has finished executing.
  • Each time the outer function is called, a new set of closed-over variables is created for the returned inner function, making them private to that specific closure instance.
  • Variables held by closures are not garbage collected as long as the closure itself is reachable.

Common Mistakes:

  • Believing that count is shared between counter1 and counter2.
  • Assuming count is garbage collected after createCounter completes.

Follow-up: Describe a scenario where improper use of closures could lead to memory leaks in a long-running application.


Question 9: Tricky Coercion with + Operator

What is the result of 1 + "2" + 3 in JavaScript?

A. "123" B. "33" C. 6 D. NaN

Correct Answer: A

Explanation: This question tests the behavior of the + operator, which can perform both addition and string concatenation based on its operands.

  1. 1 + "2":

    • The + operator encounters a number (1) and a string ("2").
    • When one operand is a string, the + operator performs string concatenation.
    • The number 1 is coerced into a string "1".
    • Result: "1" + "2" becomes "12".
  2. "12" + 3:

    • Now the + operator encounters a string ("12") and a number (3).
    • Again, due to the presence of a string, string concatenation occurs.
    • The number 3 is coerced into a string "3".
    • Result: "12" + "3" becomes "123".

Key Points:

  • The + operator has a dual role: arithmetic addition and string concatenation.
  • If any operand of + is a string, the entire operation (from left to right) tends towards string concatenation.
  • Operations are evaluated from left to right.

Common Mistakes:

  • Assuming all numbers will be added first, then concatenated.
  • Incorrectly predicting the order of coercion.

Follow-up: What would be the output of 1 + + "2" + 3? Explain why.


Question 10: this Binding and call/apply/bind

What will be logged to the console?

const person = {
  name: 'Charlie',
  greet: function(city) {
    console.log(`Hello, I'm ${this.name} from ${city}.`);
  }
};

const anotherPerson = {
  name: 'Diana'
};

person.greet.call(anotherPerson, 'New York');

A. Hello, I'm Charlie from New York. B. Hello, I'm Diana from New York. C. Hello, I'm undefined from New York. D. TypeError: Cannot read properties of undefined (reading 'name')

Correct Answer: B

Explanation: This question tests your understanding of explicit this binding using the call method.

  1. person.greet method: greet is a regular function, so its this context is determined by how it’s called.
  2. call() method: The call() method is used to invoke a function with a specified this value and arguments provided individually.
    • person.greet.call(anotherPerson, 'New York') means:
      • Execute the greet function.
      • Set this inside greet to anotherPerson.
      • Pass 'New York' as the first argument to greet (which maps to city).
  3. Output: Inside greet, this.name will now refer to anotherPerson.name, which is 'Diana'. The city parameter will be 'New York'.

Key Points:

  • call(), apply(), and bind() are methods available on all functions in JavaScript.
  • They allow you to explicitly set the this context for a function call.
  • call() takes arguments individually, apply() takes arguments as an array, and bind() returns a new function with this permanently bound.

Common Mistakes:

  • Forgetting that call/apply/bind override the default this binding rules.
  • Confusing call with apply or bind.

Follow-up: How would the syntax change if you wanted to use apply instead of call for the same outcome? What if you wanted to create a new function dianaGreet that always greets as Diana from New York, without immediately invoking it?


Practical Tips for Mastering Tricky JavaScript

  1. Deep Dive into ECMAScript Specification: For true mastery, occasionally refer to the official ECMAScript Language Specification (ES2025/2026). Understanding the spec’s abstract operations (e.g., ToPrimitive, ToBoolean, [[Call]]) demystifies coercion, this binding, and execution flow.
  2. Practice, Practice, Practice: The best way to understand these concepts is by writing code and experimenting. Use online sandboxes (e.g., JSFiddle, CodePen, browser console) to test your hypotheses.
  3. Trace Execution Manually: For complex code puzzles, meticulously trace the execution line by line, keeping track of variable scopes, this context, and the event queue. Use a debugger to step through code.
  4. Understand the “Why”: Don’t just memorize answers. For each tricky question, understand why JavaScript behaves that way. Is it due to hoisting rules? Coercion algorithms? The event loop’s microtask/macrotask priority?
  5. Focus on Core Concepts: While frameworks are important, these “weird parts” are fundamental to JavaScript itself. A strong grasp here will make you a better debugger and architect regardless of the libraries you use.
  6. Read Authoritative Articles/Books: Supplement your learning with well-regarded articles and books that specifically cover advanced JavaScript topics, such as “You Don’t Know JS” series by Kyle Simpson, or articles from MDN Web Docs.
  7. Whiteboard Practice: Explain these concepts to yourself or a peer on a whiteboard. Articulating the rules and drawing diagrams (e.g., for event loop or prototype chains) solidifies understanding.

Summary

This MCQ challenge has pushed you to think critically about JavaScript’s nuances, from hoisting and coercion to the intricacies of the event loop and this binding. Mastering these concepts is a hallmark of a proficient JavaScript developer and is essential for architecting robust, performant, and bug-free applications.

Continue to explore these areas, challenge your assumptions, and always strive to understand the underlying mechanisms. Your ability to navigate these “weird parts” confidently will distinguish you in any technical interview and empower you to write superior JavaScript code.

References

  1. MDN Web Docs (Mozilla Developer Network): The most comprehensive and authoritative resource for JavaScript documentation. Essential for understanding core concepts and APIs. (https://developer.mozilla.org/en-US/docs/Web/JavaScript)
  2. ECMAScript Language Specification: For the deepest understanding of how JavaScript works under the hood, refer to the official standard. (Search for “ECMAScript 2025 Specification” or “ECMAScript Language Specification” for the latest version, usually found on tc39.es or ecma-international.org)
  3. “You Don’t Know JS Yet” by Kyle Simpson: A highly recommended book series that dives deep into JavaScript’s core mechanisms, including scope, closures, this, and prototypes. (Available on GitHub or various book retailers)
  4. JavaScript Visualizer Tools (e.g., Loupe by Philip Roberts): Interactive tools that visualize the call stack, event queue, and microtask queue can be invaluable for understanding the event loop. (Search for “JavaScript event loop visualizer”)
  5. GeeksforGeeks - JavaScript Interview Questions: Offers a wide range of interview questions, including advanced and tricky ones, often with detailed explanations. (https://www.geeksforgeeks.org/javascript-interview-questions-and-answers/)
  6. Medium - Advanced JavaScript Interview Questions: Many skilled developers share insights and tricky questions on Medium. Searching for “Advanced JavaScript Interview Questions 2026” often yields good results. (e.g., articles similar to the search results provided)

This interview preparation guide is AI-assisted and reviewed. It references official documentation and recognized interview preparation resources.