277 JavaScript Frontend Developer Interview Questions & Answers

133 top • 30 Amazon • 15 Apple • 29 Google • 18 Meta • 21 Microsoft • 20 Netflix • 11 NVIDIA

JavaScript Frontend Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

31. How do default parameters behave when an argument is omitted or explicitly `undefined`?Language SpecificEasy

Question Details

Use a function formatPrice(amount, currency = 'USD') and compare calls with one argument, undefined, null, and an empty string. Explain when the default expression runs, its evaluation time, and how earlier parameters can be referenced by later default expressions.

Short Interview Answer (30-60 seconds)

JavaScript uses a default parameter when the argument is omitted or its value is explicitly undefined. It does not use the default for null, an empty string, zero, or false. The default expression is evaluated when the function is called and only when that parameter needs the default. A later default parameter can also reference an earlier parameter because parameters are initialized from left to right.

Detailed Explanation

See the Code while reading this explanation.

A default parameter gives a function a value to use when the caller does not provide that argument. JavaScript also uses the default when the caller explicitly provides undefined. Other supplied values stay unchanged. For example, null stays null, and an empty string stays an empty string. The default value is worked out when the function is called, not once when the function is created. This makes default parameters useful when a function has a normal value that most callers should not need to provide.

Useful Questions to Ask the Interviewer
  1. Should I explain how null and an empty string differ from undefined?
  2. Should I also show how an earlier parameter can be used by a later default expression?
How do default parameters behave when an argument is omitted or explicitly `undefined`? diagram
How to Explain It in an Interview

With formatPrice(amount, currency = 'USD'), calling formatPrice(20) makes currency equal to USD because the second argument is omitted. Calling formatPrice(20, undefined) gives the same result because an explicit undefined also activates the default.

Calling formatPrice(20, null) is different. JavaScript keeps null, so the default expression does not run. Calling formatPrice(20, '') also keeps the empty string. The rule is specific: only an omitted argument or a value of undefined activates the parameter default.

A default expression is evaluated during the function call when that parameter needs a default. It is not calculated once when the function is defined. This matters if the expression calls another function, reads changing state, or creates a new object. The expression can therefore produce a new result on each call that needs it.

Parameters are initialized from left to right. A later default expression can use an earlier parameter. For example, function createLabel(amount, text = String(amount)) can use amount while creating the default value for text. The reverse is not safe. Trying to read a later parameter before that later parameter has been initialized causes a ReferenceError.

Default parameters are useful for optional settings with a sensible normal value. In production code, do not rely on them when null, an empty string, zero, or false should also mean missing. Handle those cases explicitly inside the function.

Example

The example uses formatPrice(amount, currency = 'USD'). Omitting the second argument and passing undefined both make currency use USD. Passing null keeps null. Passing an empty string keeps the empty string. The second function shows initialization order by letting a later default expression read an earlier parameter. Default expressions are evaluated during each function call only when the corresponding parameter receives no argument or receives undefined.

Code
function formatPrice(amount, currency = 'USD') {
  // Return both values so the default parameter behavior is easy to inspect.
  return { amount, currency };
}

console.log(formatPrice(20));
console.log(formatPrice(20, undefined));
console.log(formatPrice(20, null));
console.log(formatPrice(20, ''));

function createLabel(amount, text = String(amount)) {
  // The later default can read amount because amount is initialized first.
  return text;
}

console.log(createLabel(20));
Where it is used

Default parameters are useful when a frontend function has optional settings with a normal fallback value. Common examples include a default currency, locale, page size, display mode, or configuration option. They work well when an omitted argument and undefined should mean use the normal value, while null, an empty string, zero, and false must remain meaningful values supplied by the caller.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands exactly when JavaScript uses a default parameter value. It also tests whether the candidate can distinguish an omitted argument and undefined from values such as null and an empty string. A strong answer also shows understanding of when default expressions are evaluated and how parameter initialization order affects references between parameters.

Common interview mistakes

A common mistake is thinking every falsy value activates a default parameter. That is incorrect. Null, an empty string, zero, and false are preserved. Another mistake is thinking the default expression is calculated when the function is defined. It is evaluated during a function call when that parameter needs the default. Candidates may also forget that parameters are initialized from left to right. A later default can use an earlier parameter, but trying to read a later parameter before it has been initialized causes a ReferenceError.

Interview tip

State the main rule first: an omitted argument and undefined use the default, while null and other supplied values do not. Then compare the four formatPrice calls. Finish by explaining that default expressions run during the call and that a later default expression can reference an earlier parameter.

Interviewer may ask next
What happens if the caller passes null, zero, false, or an empty string to a parameter with a default value?

The supplied value is kept, so the default does not run. JavaScript activates a default parameter only when the argument is omitted or its value is undefined. This matters because null, zero, false, and an empty string may carry real meaning in an application. If any of those values should also mean missing, the function needs an explicit check for that behavior.

Can a default expression use another parameter, and when is that expression evaluated?

Yes. A later default expression can reference an earlier parameter because parameters are initialized from left to right. For example, function createLabel(amount, text = String(amount)) can use amount while initializing text. The expression is evaluated during each function call only when the argument is omitted or undefined. Trying to read a later parameter before it has been initialized instead causes a ReferenceError. This initialization order matters when defaults depend on other arguments.

32. How do arrow functions differ from regular functions?Language SpecificEasy

Question Details

Compare an arrow function and a regular function used as an object method, a callback, and a constructor attempt. Discuss lexical this, the absence of an own arguments object, constructability, prototype presence, and when concise expression bodies improve readability without changing behavior.

Short Interview Answer (30-60 seconds)

I use arrow functions mainly for callbacks and other cases where I want this to come from the surrounding scope. I use regular functions when the caller should determine this or when I need an arguments object or constructor behavior. Arrow functions do not create their own this or arguments binding, cannot be called with new, and do not have a prototype property for constructing instances. Their concise expression body can make a simple callback easier to read without changing the result.

Detailed Explanation

See the Code while reading this explanation.

The practical difference is about how each kind of function gets information and how it can be called. One kind can receive its object context from the call itself. The other keeps the context from the place where it was created. This matters when a function is used as an object method, passed as a callback, or used to create an object. The shorter form is often convenient for small callbacks, but it cannot replace the regular form in every case. Choosing the right form prevents unexpected values and invalid constructor calls.

Useful Questions to Ask the Interviewer
  1. Should the object method use the object as its this value?
  2. Should the callback keep this from its surrounding function?
  3. Does the function need to support calls with new?
How do arrow functions differ from regular functions? diagram
How to Explain It in an Interview

A regular function gets its this value from how it is called. For example, if obj.show() calls a regular function stored as show, this normally refers to obj. An arrow function does not create its own this binding. It reads this from the surrounding lexical scope. Because of that, an arrow is usually a poor choice for an object method when the method needs this to refer to the object receiving the call.

This same behavior is useful for callbacks. An arrow callback can keep the this value of its surrounding function without bind or a saved variable.

A regular function also receives its own arguments object when it is called. An arrow function does not create an arguments binding. If an enclosing regular function has arguments, the arrow can read that outer binding. In modern code, rest parameters such as (...args) are usually clearer when an arrow needs a list of supplied values.

Arrow functions are never constructable, so calling one with new throws a TypeError. They also do not have their own prototype property. An ordinary function declaration or function expression is normally constructable and has a prototype property. However, not every non arrow function is constructable. For example, method definitions in object literals and classes cannot be called with new and do not have a prototype property for construction.

For a simple expression, an arrow can return the expression without writing return. For example, x => x * 2 returns the calculated value. This changes the syntax, not the result of the calculation. It is useful when the shorter form is easier to read.

Example

The example uses one object to show how this differs, one regular outer function to show an arrow callback keeping the surrounding this value, and constructor checks to show constructability and prototype behavior. The regular object method receives the object as this because it is called through that object. The arrow method does not get a new this binding from the method call. The regular function receives its own arguments object, while the arrow callback uses a rest parameter. An ordinary function declaration can be called with new and has a prototype property. The arrow constructor attempt throws a TypeError and the arrow has no own prototype property.

Code
const example = {
  value: 10,

  // This regular function receives this from the method call.
  regularMethod: function () {
    console.log('regular method:', this.value);

    // A regular function receives its own arguments object.
    console.log('regular arguments count:', arguments.length);
  },

  // This arrow keeps this from the surrounding scope instead of this object.
  arrowMethod: () => {
    console.log('arrow method this value:', this?.value);
  },
};

example.regularMethod('one', 'two');
example.arrowMethod();

function runCallback() {
  const outerThis = this;

  // The arrow keeps the this binding from runCallback.
  // A rest parameter gives it an explicit list of supplied values.
  const callback = (...args) => {
    console.log('callback this matches:', this === outerThis);
    console.log('callback rest count:', args.length);
  };

  callback('a', 'b');
}

runCallback.call({ name: 'frontend' });

function RegularConstructor(name) {
  // When called with new, this is the newly created instance.
  this.name = name;
}

// An ordinary function declaration is constructable.
const regularInstance = new RegularConstructor('Ada');
console.log('regular instance:', regularInstance.name);
console.log('regular prototype exists:', Object.hasOwn(RegularConstructor, 'prototype'));

const ArrowConstructor = (name) => ({ name });

// An arrow has no own prototype property and cannot be used with new.
console.log('arrow prototype exists:', Object.hasOwn(ArrowConstructor, 'prototype'));

try {
  // This attempt is invalid because arrow functions are not constructable.
  new ArrowConstructor('Ada');
} catch (error) {
  console.log('arrow constructor error:', error instanceof TypeError);
}

// A concise expression body returns the expression automatically.
const double = (value) => value * 2;
console.log('concise result:', double(5));
Where it is used

Arrow functions are common in frontend callbacks such as array mapping, filtering, Promise handlers, and helper callbacks that should keep this from an enclosing function. Regular functions are useful for object methods that need this from the call site, functions that need their own arguments object, and ordinary constructor functions that are intentionally called with new. In production code, the choice should follow the required behavior rather than using arrow syntax only because it is shorter. For simple callbacks, the performance and memory difference is usually not a useful reason to choose one form over the other.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands that arrow functions are not just shorter regular functions. They want to see whether the candidate understands lexical this, the arguments binding, constructor behavior, prototype presence, and how these differences affect practical frontend code.

Common interview mistakes

A common mistake is thinking an arrow function is only shorter syntax for a regular function. Another mistake is using an arrow as an object method and expecting this to refer to the object that made the call. Developers may also expect an arrow to create its own arguments object, even though it can only read an outer arguments binding if one exists. Another mistake is calling an arrow with new or expecting it to have its own prototype property. It is also incorrect to assume every regular looking function is constructable, because object and class method definitions cannot be called with new.

Interview tip

Start with lexical this because it is the most important practical difference. Then mention arguments, new, and prototype. Also point out that ordinary function declarations can be constructors, while object and class method definitions are not constructable. Finish by explaining that concise arrow bodies are mainly a readability choice.

Interviewer may ask next
What happens if call, apply, or bind is used with an arrow function to change this?

They cannot change the this value used by an arrow function. The arrow gets this lexically from its surrounding scope. call and apply can still provide arguments, and bind can create a new function with preset arguments, but the requested this value does not replace the arrow's lexical this. This matters when code depends on changing this at call time.

Should arrow functions always be preferred for callbacks in production code?

No. Arrow functions are useful when a callback should keep this from the surrounding scope or when concise syntax improves readability. A regular function is better when the callback needs this to be supplied by the caller or needs its own arguments object. Performance is usually not the deciding factor. The main tradeoff is choosing the function semantics that match the required runtime behavior.

33. What is the iterator protocol?Language SpecificMedium

Question Details

Build an object whose [Symbol.iterator]() method returns an iterator with next() results. Explain the shape of {value, done}, how for...of, spread, and array destructuring consume the protocol, and what optional return() cleanup may be triggered when iteration stops early.

Short Interview Answer (30-60 seconds)

The iterator protocol is the standard rule JavaScript uses to read values one at a time. An iterable provides a Symbol.iterator method that returns an iterator. The iterator has a next method that returns an object with value and done. Features such as for...of, spread, and array destructuring call these methods automatically. An iterator can also provide return so it can clean up resources when a consumer stops early.

Detailed Explanation

See the Code while reading this explanation.

The main idea is that an object can provide its items one at a time instead of creating every item first. Each request gives the next item or says that there are no more items. This lets common language features read the same object in a predictable way. We can build an object that produces the numbers 1, 2, and 3. We should also understand what happens when reading reaches the end and what may happen when the reader stops before all three values have been read.

Useful Questions to Ask the Interviewer
  1. Should I show a custom object that produces a fixed sequence of values?
  2. Should I also demonstrate cleanup when a loop stops early?
What is the iterator protocol? diagram
How to Explain It in an Interview

An object is iterable when it has a method stored at Symbol.iterator. JavaScript calls that method to get an iterator. The iterator must have a next method.

Each call to next returns an object. While a value is available, it can return { value: 1, done: false }. When the sequence has finished, it can return { done: true }. The value property is optional when done is true.

In this example, Symbol.iterator creates fresh iteration state by starting a counter at 1. This matters because separate consumers should normally be able to iterate independently. The next method returns 1, then 2, then 3, and then reports completion.

A for...of loop gets the iterator and repeatedly calls next until done becomes true. Spread also consumes the iterator until completion and puts the produced values into a new array. Array destructuring requests only the values it needs. If destructuring stops before the iterator is finished, JavaScript closes the iterator and calls its return method when that method exists and is callable.

A break from for...of also closes an unfinished iterator. This lets an optional return method perform cleanup. Normal completion does not call return just because iteration reached done.

Custom iterables are useful when an object naturally represents a sequence. They can produce values only when requested, so a complete result array does not have to exist first. However, spread still creates a new array containing every produced value. The iterator protocol here is synchronous. Asynchronous sequences use the async iterator protocol instead.

Example

The example creates an iterable object whose Symbol.iterator method creates fresh state for each consumer. The iterator keeps a current number. next returns 1, 2, and 3 with done set to false, then returns done set to true. The optional return method records that early cleanup happened and returns done set to true. Spread demonstrates normal full consumption. The for...of loop demonstrates early termination with break, which closes the unfinished iterator and calls its return method.

Code
const numberSequence = {
  [Symbol.iterator]() {
    // Give each consumer its own position so separate iterations do not share progress.
    let current = 1;
    let closedEarly = false;

    return {
      next() {
        // Produce the next number while the sequence still has a value available.
        if (current <= 3) {
          return { value: current++, done: false };
        }

        // Report normal completion after all three numbers have been produced.
        return { done: true };
      },

      return() {
        // Record cleanup when a consumer closes this iterator before normal completion.
        closedEarly = true;
        console.log('cleanup called', closedEarly);
        return { done: true };
      },
    };
  },
};

// Spread consumes the iterable until next reports completion.
console.log([...numberSequence]);

// Breaking before completion closes the iterator and calls its return method.
for (const value of numberSequence) {
  console.log(value);
  if (value === 2) {
    break;
  }
}
Where it is used

Custom iterables are useful for data structures that should expose values in a controlled order, sequences that produce values only when requested, and wrappers around resources that need cleanup when reading stops early. They also let application objects work naturally with for...of, spread, and destructuring. Producing values on demand can avoid allocating a complete result array before iteration begins, although consumers such as spread still allocate their own result array.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands the rules JavaScript uses to produce values one at a time. They want to see whether the candidate can create a custom iterable, explain how built in language features consume it, and handle cleanup when a consumer stops before all values are produced.

Common interview mistakes

A common mistake is returning an object from Symbol.iterator that does not provide the required next method. Another mistake is returning raw values from next instead of result objects containing value and done. Developers may also keep one shared position on the iterable itself, which can make separate consumers interfere with each other. Another misunderstanding is assuming return runs after every successful iteration. It is used during iterator closing when consumption stops before normal completion, not simply because next eventually returned done.

Interview tip

Start with the contract: Symbol.iterator returns an iterator, and next returns an object with value and done. Then explain one concrete sequence such as 1, 2, and 3. Finally mention that for...of, spread, and destructuring consume the protocol automatically, and that return can support cleanup when consumption stops early.

Interviewer may ask next
What happens to the iterator if a for...of loop uses break before iteration is complete?

JavaScript closes the unfinished iterator. If the iterator has a callable return method, JavaScript calls it before leaving the loop. This matters because return gives the iterator a place to release resources or perform other cleanup after the consumer stops requesting values.

When would you use a custom iterable instead of first building an array?

I would use a custom iterable when values can be produced one at a time and creating the complete array first is unnecessary. The iterable can delay work and avoid allocating one full result array before consumption begins. The tradeoff is more implementation code, and consumers such as spread still create a new array containing all produced values.

34. How does JavaScript convert an object to a primitive value?Language SpecificMedium

Question Details

Trace abstract conversion for an object used with string concatenation, numeric addition, and a relational comparison. Cover Symbol.toPrimitive, valueOf, and toString lookup order for number and string hints. Include a small object with observable methods so the call sequence is unambiguous.

Short Interview Answer (30-60 seconds)

JavaScript first tries Symbol.toPrimitive when an object must become a primitive. It passes a hint such as default, number, or string. If that method is absent, ordinary conversion tries valueOf before toString for a number hint, and toString before valueOf for a string hint. The plus operator normally starts with a default hint, while relational comparison uses a number hint. After primitive conversion, the operator continues with the primitive values it received.

Detailed Explanation

See the Code while reading this explanation.

JavaScript sometimes needs a simple value from an object before it can perform an operation. For example, an object may appear beside text, beside a number, or inside a comparison. JavaScript then asks the object for a simpler value. The object can control what value it gives back. The exact method that runs depends on the kind of conversion JavaScript needs. This is why the same object can behave differently in different expressions. Understanding the order helps you predict results and avoid surprising behavior in real code.

Useful Questions to Ask the Interviewer
  1. Should I explain both Symbol.toPrimitive and the valueOf and toString fallback behavior?
  2. Would you like me to trace the exact calls for plus and relational comparison?
How does JavaScript convert an object to a primitive value? diagram
How to Explain It in an Interview

JavaScript uses the abstract ToPrimitive operation when an object must become a primitive value.

First, JavaScript looks for object[Symbol.toPrimitive]. If that property exists and is callable, JavaScript calls it with a hint. The hint is "default", "number", or "string". The method must return a primitive value. If it returns an object, JavaScript throws a TypeError.

If Symbol.toPrimitive is absent, JavaScript uses ordinary conversion. With a number hint, it tries valueOf first and then toString. With a string hint, it tries toString first and then valueOf. JavaScript stops as soon as one of those methods returns a primitive.

For most ordinary objects, a default hint is handled like a number hint during this fallback. Some built in objects can have special default behavior, so it is safer to describe the default hint separately from the number hint.

The plus operator asks object operands for primitives using the default hint. After that conversion, if either primitive is a string, plus performs string concatenation. Otherwise it performs numeric addition after numeric conversion. So an expression such as object + " items" does not request a string hint merely because the other operand is a string.

A relational comparison such as object < 20 requests primitive conversion with a number hint before comparing the resulting values.

Explicit String(object) requests string oriented primitive conversion. Explicit Number(object) requests number oriented primitive conversion.

In production code, custom coercion should be used carefully. Symbol.toPrimitive is useful when a value object intentionally needs controlled conversion behavior. Explicit conversion is often easier to read because another developer can see the intended type directly.

Example

The example uses one observable object so every conversion method records when it runs. While Symbol.toPrimitive exists, JavaScript calls it before valueOf or toString. String(probe) passes the string hint. probe + 5 passes the default hint because plus requests default primitive conversion. probe < 20 passes the number hint. The example then removes Symbol.toPrimitive to expose ordinary fallback behavior. Number(probe) uses the number hint, so valueOf runs first. String(probe) uses the string hint, so toString runs first. Finally, probe + " items" uses the default hint. For this ordinary object, fallback treats that default like a number hint, so valueOf returns 10 and the final operation produces the string "10 items".

Code
const calls = [];

const probe = {
  // Record number oriented fallback and return a primitive number.
  valueOf() {
    calls.push('valueOf');
    return 10;
  },

  // Record string oriented fallback and return a primitive string.
  toString() {
    calls.push('toString');
    return 'ten';
  },

  // Record the exact hint supplied by JavaScript before fallback is considered.
  [Symbol.toPrimitive](hint) {
    calls.push(`Symbol.toPrimitive:${hint}`);

    // Return a string only for an explicit string hint so each case is easy to observe.
    if (hint === 'string') {
      return 'custom ten';
    }

    // Return a number for default and number hints.
    return 10;
  },
};

// Explicit String conversion supplies the string hint.
console.log(String(probe));
console.log(calls.splice(0));

// Plus supplies the default hint before deciding between concatenation and numeric addition.
console.log(probe + 5);
console.log(calls.splice(0));

// Relational comparison supplies the number hint for object primitive conversion.
console.log(probe < 20);
console.log(calls.splice(0));

// Remove the custom primitive hook so the ordinary fallback order can be observed.
delete probe[Symbol.toPrimitive];

// Explicit Number conversion uses the number hint, so valueOf is tried first.
console.log(Number(probe));
console.log(calls.splice(0));

// Explicit String conversion uses the string hint, so toString is tried first.
console.log(String(probe));
console.log(calls.splice(0));

// Plus still supplies the default hint. For this ordinary object, fallback tries valueOf first.
console.log(probe + ' items');
console.log(calls.splice(0));
Where it is used

This behavior appears when objects are used with operators, comparisons, explicit String or Number conversion, and custom value objects. A class representing money, a measurement, or another domain value may define Symbol.toPrimitive so conversion has deliberate behavior. It can also appear accidentally when application objects reach expressions that trigger coercion. In most production code, explicit property access or explicit conversion is easier to understand and maintain because the intended value is visible.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands JavaScript coercion beyond simple primitive conversions. A strong answer shows that the candidate knows how an object becomes a primitive, understands conversion hints, knows the priority of Symbol.toPrimitive, and can predict whether valueOf or toString runs. This knowledge matters when debugging operators, comparisons, formatting, custom value objects, and unexpected coercion in frontend code.

Common interview mistakes

A common mistake is saying that plus asks an object for a string whenever the other operand is a string. It does not. Plus first requests primitive conversion with the default hint, then decides whether to concatenate after it has primitive values. Another mistake is saying valueOf always runs before toString. A number hint tries valueOf first, while a string hint tries toString first. Candidates also sometimes forget that Symbol.toPrimitive has priority over both fallback methods. Another important mistake is returning an object from Symbol.toPrimitive. That method must return a primitive, or JavaScript throws a TypeError.

Interview tip

Start with Symbol.toPrimitive because it has first priority. Then state the fallback orders clearly. A number hint tries valueOf and then toString. A string hint tries toString and then valueOf. Next, explain that plus uses the default hint and relational comparison uses the number hint. Finish with one observable example so the interviewer can see that you understand the exact call sequence.

Interviewer may ask next
What happens if Symbol.toPrimitive returns another object instead of a primitive?

JavaScript throws a TypeError. Symbol.toPrimitive is required to return a primitive value such as a string, number, bigint, boolean, symbol, null, or undefined. JavaScript does not continue to valueOf or toString after Symbol.toPrimitive returns an object. This matters because the custom conversion hook has a strict result requirement, and violating it makes the whole conversion fail.

Should production code rely on implicit object conversion or use explicit conversion?

Explicit conversion is usually clearer in production code. Implicit conversion can be useful when an object intentionally defines stable value semantics, and Symbol.toPrimitive gives precise control over that behavior. The tradeoff is readability. An implicit expression can be shorter, but String, Number, or explicit property access usually makes the intended value easier for another developer to understand and reduces surprises during maintenance.

35. Why is `this` often lost when a method is passed as a callback?Language SpecificMedium

Question Details

Pass panel.open directly to an event listener and compare it with an arrow wrapper and a bound method. Explain how the callback invocation supplies its own receiver, why lexical scope does not preserve a normal method's this, and how removal of a bound listener requires retaining the same function reference.

Short Interview Answer (30-60 seconds)

A normal method does not permanently remember its object as this. If I pass panel.open directly to an event listener, the browser later calls that function as the listener, so this comes from that callback invocation and is not the panel object. I can use an arrow wrapper that calls panel.open() or use panel.open.bind(panel). If I use bind, I keep the returned function so I can pass that exact same reference to removeEventListener.

Detailed Explanation

See the Code while reading this explanation.

Passing a method by itself means we give another part of the program the function, but not the object that normally calls it. Later, the event system calls that function in its own way. Because of this, the method may see a different object when it tries to use this. Two common fixes are to put the method call inside an arrow function or to create a bound function that always uses the intended object. A bound function should be saved because removing an event listener requires the same function value that was originally added.

Useful Questions to Ask the Interviewer
  1. Should I compare direct method passing, an arrow wrapper, and bind using a browser event listener?
  2. Should I also explain how to correctly remove the bound listener?
Why is `this` often lost when a method is passed as a callback? diagram
How to Explain It in an Interview

The key rule is that a normal JavaScript function gets its this value from the way it is called. Lexical scope does not preserve this for a normal function. The place where the function was written does not permanently attach a this value to it.

Suppose panel.open() reads this.name. When we call panel.open(), the call has panel before the dot, so this is panel.

If we instead pass panel.open to addEventListener, we pass only the function value. When the browser later invokes a normal event listener, it calls the listener with the event current target as this. In this example, that is the button, not panel.

An arrow wrapper such as () => panel.open() fixes the problem because the wrapper explicitly performs panel.open(). That method call makes panel the receiver. Arrow functions have lexical this, meaning they do not create their own dynamic this, but this example does not depend on the wrapper's this. It works because the wrapper calls open through panel.

Another option is panel.open.bind(panel). bind creates a new function whose this value is fixed to panel when that function runs.

The important production detail is function identity. Every call to bind creates a different function object. Calling removeEventListener with a newly created bound function will therefore not remove the original listener. Store the bound function and reuse the same reference. The same rule applies to an arrow wrapper if that listener must later be removed.

Example

The example creates one panel object and three buttons. The direct listener receives panel.open by itself, so the browser invokes that normal listener with the button as this. The arrow listener explicitly calls panel.open(), so the method receives panel as this. The bound listener uses a function created once with panel.open.bind(panel), which fixes this to panel. The bound function is stored and the exact same reference is later passed to removeEventListener. A new call to bind would create a different function and would not remove the original listener.

Code
const panel = {
  name: 'Settings panel',

  open() {
    // Show which object JavaScript supplied as this for this call.
    console.log(this === panel ? this.name : `different receiver: ${this.tagName}`);
  },
};

const directButton = document.createElement('button');
directButton.textContent = 'Direct method';

const arrowButton = document.createElement('button');
arrowButton.textContent = 'Arrow wrapper';

const boundButton = document.createElement('button');
boundButton.textContent = 'Bound method';

document.body.append(directButton, arrowButton, boundButton);

// Passing only the method removes the panel receiver from the later callback invocation.
directButton.addEventListener('click', panel.open);

// Store the wrapper so the exact same callback can be used for cleanup later.
const arrowListener = () => {
  // Calling through panel makes panel the receiver of open.
  panel.open();
};
arrowButton.addEventListener('click', arrowListener);

// bind creates a new function whose this value is fixed to panel.
const boundOpen = panel.open.bind(panel);
boundButton.addEventListener('click', boundOpen);

// Trigger each listener so the example runs without manual clicks.
directButton.dispatchEvent(new Event('click'));
arrowButton.dispatchEvent(new Event('click'));
boundButton.dispatchEvent(new Event('click'));

// Removal succeeds because this is the exact callback reference that was added.
boundButton.removeEventListener('click', boundOpen);
Where it is used

This behavior appears often in frontend code when object methods are used as DOM event listeners, timers, subscription callbacks, or callbacks passed into other APIs. In browser interfaces, developers often store an arrow wrapper or a bound method when a component starts, then reuse the same function reference when the component is cleaned up. This keeps the intended receiver clear and allows listener cleanup to use the correct callback reference.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands that a normal function gets this from how the function is called, not from where the function was written. They also want to see whether the candidate can choose a safe callback pattern and correctly add and remove browser event listeners.

Common interview mistakes

A common mistake is thinking that panel.open remembers panel because the method was defined on that object. A normal function does not work that way. Another mistake is saying that lexical scope preserves the this value of a normal method. Lexical scope preserves normal variable bindings, while a normal function's this depends on how the function is invoked. Another common bug is adding panel.open.bind(panel) and later trying to remove it with another panel.open.bind(panel). Those are different function objects, so the listener remains registered. Inline arrow wrappers have the same reference problem when cleanup is required.

Interview tip

Start with the call site rule: a normal function gets this from how it is called. Then compare panel.open, () => panel.open(), and panel.open.bind(panel). Finish by mentioning that bind creates a new function, so the same stored reference must be used for listener removal.

Interviewer may ask next
What happens if I call `removeEventListener("click", panel.open.bind(panel))` after adding another bound version of the method?

It does not remove the original listener. Each call to bind creates a new function object, so the callback passed to removeEventListener has a different identity from the callback that was added. For listener removal, the browser must match the event type, the callback reference, and the capture setting. Other options such as once and passive do not change that matching rule. This matters because repeatedly creating bound functions can leave old listeners attached. Store the original bound function and reuse that exact reference.

Should I prefer an arrow wrapper or `bind` for this kind of event listener?

Either can be correct, and the choice depends on what the callback needs to do. An arrow wrapper such as () => panel.open() is simple when I want to call a method and possibly add extra logic or arguments. bind is useful when I want a reusable function whose this is fixed to panel. Both approaches create a function object that should be stored when later removal is required. The main production concern is keeping a stable callback reference and making the intended receiver clear.

36. What makes a copy of a JavaScript object shallow rather than deep?Language SpecificMedium

Question Details

Use an object containing nested arrays, a Date, a Map, and a shared child referenced from two properties. Compare object spread, Object.assign, structuredClone, and a JSON round trip. Explain which identities are preserved, which built-in types survive, and how cycles or functions affect each approach.

Short Interview Answer (30-60 seconds)

A copy is shallow when only the outer object is new while nested objects still refer to the same objects as the original. Object spread and Object.assign make shallow copies. structuredClone is a browser API that creates independent nested values for supported data and preserves relationships such as two properties pointing to the same cloned child. A JSON round trip can create independent plain data, but it changes or loses some JavaScript types and cannot handle cycles.

Detailed Explanation

See the Code while reading this explanation.

A shallow copy gives you a new outer container, but some values inside it still belong to the original data. If a nested list or child object changes through one copy, the other can see that change too. A deep copy creates separate nested values, so later changes do not affect the original. The choice matters when data contains dates, maps, repeated references, cycles, or functions, because each copying method handles these values differently. The key question is whether nested values keep sharing the same underlying objects or become independent copies.

Useful Questions to Ask the Interviewer
  1. Should the copied value support cycles and shared references?
  2. Do Date and Map values need to keep their original types?
  3. Should functions be copied, rejected, or kept by reference?
What makes a copy of a JavaScript object shallow rather than deep? diagram
How to Explain It in an Interview

Object spread and Object.assign copy the source object's own enumerable properties into a new outer object. Primitive values are copied directly. For an object valued property, the copied value is a reference to the same nested object. That is why the result is shallow.

Suppose the source contains a nested array, a Date, a Map, and one child object referenced by both left and right. After using object spread or Object.assign, the outer object has a new identity, but the nested array, Date, Map, and shared child still have the same identities as in the source. left and right still point to the same child.

In evergreen browsers, structuredClone is a browser API that uses the structured clone algorithm. It creates new nested objects, arrays, Date values, and Map values for supported data. A Date remains a Date. A Map remains a Map. If left and right originally reference the same child, both properties in the clone reference the same new cloned child. The clone does not share that child with the original. Cyclic references are also preserved. Ordinary functions are not supported, so structuredClone throws a DataCloneError when it reaches one.

A JSON round trip using JSON.stringify followed by JSON.parse can create separate nested plain data, but it is not a general deep clone. Date values become strings. Map values normally become empty plain objects unless custom conversion is provided. Repeated references lose their shared identity because each occurrence is serialized separately. Cycles make JSON.stringify throw a TypeError. Function valued object properties are omitted during serialization.

In production, object spread or Object.assign is useful when only the outer level needs to change and nested sharing is intentional. structuredClone is useful when supported nested data must be independent. A deep clone can take more time and memory because it must visit and allocate nested values.

Example

The example creates one source object containing a nested array, a Date, a Map, and one shared child referenced by two properties. Spread and Object.assign create new outer objects but keep the original nested identities. structuredClone creates independent supported nested values while preserving the fact that left and right refer to one shared child inside the clone. The JSON round trip creates separate plain data, converts the Date to a string, turns the Map into an empty plain object with normal serialization, and does not preserve the shared child identity. Separate checks show that structuredClone supports cycles but rejects ordinary functions, while JSON serialization rejects cycles and omits function valued object properties.

Code
const sharedChild = { count: 1 };

const original = {
  items: [{ name: 'A' }],
  createdAt: new Date('2026-01-01T00:00:00Z'),
  lookup: new Map([['theme', 'dark']]),
  left: sharedChild,
  right: sharedChild,
};

// These methods create a new outer object but keep nested reference identities.
const spreadCopy = { ...original };
const assignCopy = Object.assign({}, original);

console.log(spreadCopy !== original);
console.log(spreadCopy.items === original.items);
console.log(assignCopy.lookup === original.lookup);
console.log(spreadCopy.left === spreadCopy.right);

// structuredClone creates independent supported nested values and preserves internal sharing.
const deepCopy = structuredClone(original);

console.log(deepCopy !== original);
console.log(deepCopy.items !== original.items);
console.log(deepCopy.createdAt instanceof Date);
console.log(deepCopy.lookup instanceof Map);
console.log(deepCopy.left === deepCopy.right);
console.log(deepCopy.left !== original.left);

// JSON serialization recreates data but does not preserve every JavaScript type or identity relationship.
const jsonCopy = JSON.parse(JSON.stringify(original));

console.log(typeof jsonCopy.createdAt);
console.log(jsonCopy.lookup);
console.log(jsonCopy.left === jsonCopy.right);

// A cycle is supported by structuredClone but rejected by JSON.stringify.
const cyclic = { name: 'cycle' };
cyclic.self = cyclic;
const clonedCycle = structuredClone(cyclic);
console.log(clonedCycle.self === clonedCycle);

try {
  JSON.stringify(cyclic);
} catch (error) {
  console.log(error.name);
}

// Ordinary functions cannot be cloned by structuredClone.
try {
  structuredClone({ run() {} });
} catch (error) {
  console.log(error.name);
}

// A function valued object property is omitted by JSON serialization.
const jsonWithFunction = JSON.stringify({ value: 1, run() {} });
console.log(jsonWithFunction);
Where it is used

Shallow copies are common when updating state objects where only the outer object must be replaced and unchanged nested values may safely be shared. structuredClone is useful for independent snapshots, cached data copies, and editable drafts that contain supported nested values such as arrays, Date values, Maps, Sets, and cycles. Browser features such as worker messaging use the structured clone algorithm to copy supported data between execution contexts. A JSON round trip can be acceptable for simple JSON shaped data, but it should not be treated as a general JavaScript cloning method.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands object identity, nested references, copying behavior, and the limits of common copying techniques. They also want to see whether the candidate can choose an appropriate copy method when values include Date, Map, shared references, cycles, or functions.

Common interview mistakes

A common mistake is thinking that a new outer object means every nested value is also new. Another is calling object spread or Object.assign a deep copy. Some developers also use a JSON round trip as a universal clone and forget that Date, Map, shared identity, cycles, undefined values, and functions do not behave like ordinary JSON data. Another mistake is assuming structuredClone is part of the ECMAScript language or that it can clone every JavaScript value. In browsers it is a Web API, and ordinary functions are not supported.

Interview tip

Start by comparing identity. Say that spread and Object.assign create a new outer object but reuse nested object references. Then explain that the browser structuredClone API creates new supported nested values and preserves internal relationships. Finish by explaining why a JSON round trip is only suitable for simple JSON shaped data.

Interviewer may ask next
What happens if the object contains a cycle or a function?

structuredClone preserves cyclic references for supported data, but an ordinary function makes it throw a DataCloneError. JSON.stringify cannot serialize a cycle and throws a TypeError. A function stored as an object property is normally omitted by JSON.stringify. This matters because neither method should be assumed to accept every JavaScript value.

When would you choose a shallow copy instead of structuredClone?

I would choose a shallow copy when I only need a new outer object and intentionally want unchanged nested values to stay shared. Object spread or Object.assign is simpler and avoids allocating copies of the whole nested graph. structuredClone is more appropriate when supported nested data must become independent, but it can use more time and memory because it visits and allocates nested values.

37. What steps does the `new` operator perform?Language SpecificMedium

Question Details

Manually describe the behavior of new Constructor(arg) using a constructor that returns nothing, one that returns a primitive, and one that returns an object. Cover prototype linkage, this binding, execution, the return-value override rule, and what happens when the target is not constructable.

Short Interview Answer (30-60 seconds)

The new operator creates a fresh object, chooses its prototype from the constructor, calls the constructor with that new object as this, and then decides what value to return. If the constructor returns an object or function, that value replaces the fresh object. If it returns nothing or returns a primitive value, JavaScript returns the fresh object. The target must also be constructable. For example, an arrow function cannot be used with new.

Detailed Explanation

See the Code while reading this explanation.

The main idea is that new creates an object by following a fixed set of steps. JavaScript prepares a fresh object and connects it to an object used for shared behavior. It then runs the constructor while making the fresh object the current object. What the constructor returns can change the final result. Returning no value keeps the fresh object. Returning a simple value also keeps it. Returning another object replaces it. JavaScript also requires the value after new to support object construction. If it does not, JavaScript reports an error instead of creating an instance.

Useful Questions to Ask the Interviewer
  1. Should I explain both constructor functions and classes?
  2. Would you like me to show the return value rule with a small code example?
What steps does the `new` operator perform? diagram
How to Explain It in an Interview

For new Constructor(arg), JavaScript first checks that the target can be used as a constructor. If it is not constructable, new throws a TypeError.

For a constructable target, JavaScript creates a fresh ordinary object. Its prototype is normally taken from Constructor.prototype. If that property is an object, the fresh object uses it as its prototype. If that property is not an object, JavaScript uses the appropriate default intrinsic prototype instead.

Next, JavaScript runs the constructor with the fresh object as this. The arguments in the new expression are passed to the constructor. For example, this.name = name adds a name property to the fresh object.

JavaScript then applies the constructor return rule. If the constructor finishes without an explicit return value, the fresh object is returned. If it explicitly returns a primitive value such as a number, string, boolean, symbol, bigint, null, or undefined, that value does not replace the fresh object. If it returns an object or function, that returned value becomes the result instead.

So a constructor that sets this.name and returns nothing produces the fresh instance. A constructor that returns 42 still produces the fresh instance. A constructor that returns { replacement: true } produces that replacement object instead.

This behavior matters in production when working with classes, constructor functions, prototype methods, inheritance, libraries, and older frontend code. The construction itself has no useful single asymptotic complexity because the constructor can perform arbitrary work. The main allocation is the new object, unless the constructor returns another object instead.

Example

The example uses three constructable functions. ReturnsNothing stores a name on this and has no explicit return value, so new returns the fresh instance. ReturnsPrimitive stores a name and returns the number 42. JavaScript ignores that primitive result and still returns the fresh instance. ReturnsObject stores a name and then returns another object, so that returned object replaces the fresh instance. The example also uses an arrow function to show that a nonconstructable target causes new to throw a TypeError.

Code
function ReturnsNothing(name) {
  // Store state on the fresh object that JavaScript supplies as this.
  this.name = name;
  // There is no explicit return, so new returns the fresh object.
}

function ReturnsPrimitive(name) {
  // Store state on the fresh object before returning a primitive value.
  this.name = name;
  // A primitive return value does not replace the object created by new.
  return 42;
}

function ReturnsObject(name) {
  // This changes the fresh object, but that object will not be the final result.
  this.name = name;
  // An explicit object return replaces the object that new created.
  return { replacement: true, originalName: name };
}

const first = new ReturnsNothing('Ada');
const second = new ReturnsPrimitive('Grace');
const third = new ReturnsObject('Linus');

console.log(first.name);
console.log(second.name);
console.log(third);

const ArrowConstructor = () => {};

try {
  // Arrow functions are callable but not constructable, so new throws a TypeError.
  new ArrowConstructor();
} catch (error) {
  // Confirm that the failed construction produced the expected error type.
  console.log(error instanceof TypeError);
}
Where it is used

This behavior is used whenever frontend code creates instances with JavaScript classes or constructor functions. It appears in custom models, reusable components that are implemented as plain JavaScript objects, browser and library APIs that expose constructors, and older code that uses constructor functions with prototypes. Understanding the return rule is useful when a constructor unexpectedly produces a different object. Understanding prototype linkage is useful when methods are shared through a prototype instead of being created separately for every instance.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands what JavaScript actually does when creating an object with new. A strong answer shows knowledge of prototype linkage, this binding, constructor execution, the special return rule, and the requirement that the target be constructable. This knowledge helps when reading constructor functions, using classes, debugging prototype behavior, and understanding why some functions cannot be used with new.

Common interview mistakes

A common mistake is saying that new always returns whatever the constructor returns. That is false because primitive return values are ignored. Another mistake is forgetting that the fresh object's prototype normally comes from the constructor's prototype property. Candidates may also assume that every function can be used with new. Arrow functions are callable but not constructable. Another mistake is assuming that changes made through this must appear in the final result even when the constructor explicitly returns another object. It is also incorrect to treat null as an object return for this rule. Returning null does not replace the fresh instance.

Interview tip

Explain the behavior in order: verify the target is constructable, create the object, choose its prototype, call the constructor with that object as this, then apply the return rule. Give the three requested cases next: no return value, primitive return value, and object return value. Finish by mentioning that a nonconstructable target causes a TypeError.

Interviewer may ask next
What happens if a constructor explicitly returns `null`?

The fresh instance is still returned. For the constructor return rule, null does not count as an object that can replace the created instance. JavaScript therefore ignores the returned null and uses the fresh object. This matters because typeof null is "object", but that historical typeof result does not change how constructor returns are handled.

Why would you put methods on a constructor prototype instead of creating the same method inside every constructor call?

Prototype methods let many normal instances share the same function object. Because new normally links each instance to the constructor prototype, method lookup can find that shared function through the prototype chain. Creating the same method during every constructor call can allocate a separate function for each instance. Sharing a prototype method can therefore reduce repeated function allocation when many instances are created. An instance specific function is still useful when each object truly needs its own function state or behavior.

38. When do static fields and static initialization blocks run?Language SpecificMedium

Question Details

Define a base class and subclass with static fields, a computed static name, and a static initialization block that records execution. Explain evaluation order, the receiver used by static methods, inheritance of static properties, and why instance construction does not rerun static initialization.

Short Interview Answer (30-60 seconds)

Static fields and static initialization blocks run when JavaScript evaluates the class definition, not when an instance is created. They run once for that class evaluation and in the order they appear. The base class is already evaluated before the subclass static elements run. Static methods are inherited, and this inside a static method depends on which constructor receives the call. Creating objects with new does not run static initialization again.

Detailed Explanation

See the Code while reading this explanation.

Static values belong to the class itself instead of each object created from the class. JavaScript creates these values while it creates the class. It processes them from top to bottom. A child class is created after its parent class is available. The child can also access class level values and functions from its parent. Creating a normal object later does not repeat this class setup. This matters when a program needs shared settings, counters, registration information, or setup work that should happen once when a class definition is evaluated.

Useful Questions to Ask the Interviewer
  1. Should I explain both a base class and a subclass?
  2. Should I include how this behaves inside an inherited static method?
When do static fields and static initialization blocks run? diagram
How to Explain It in an Interview

A static field initializer runs while JavaScript evaluates the class definition. A static initialization block also runs during that class evaluation. Static fields and static blocks are processed in source order, so a later static element can observe values or changes produced by an earlier static element.

For a base class, its static elements finish while that class is being evaluated. When JavaScript evaluates a subclass, the base class constructor already exists. JavaScript then processes the subclass static elements in their own source order. This gives a clear sequence: base class setup first, then subclass setup.

Static properties and static methods can be inherited through the relationship between constructor objects. If Base defines a static method named record, Child can call Child.record(). Inside that call, this is Child because Child is the receiver of the call. If Base.record() is called, this is Base.

Inside a static field initializer or static block, this refers to the class constructor currently being initialized. This makes it possible to compute class level values or record initialization work.

Creating an instance with new Child() does not evaluate the class definition again. Instance construction runs the constructor and initializes instance fields, but it does not rerun static fields or static blocks. Static initialization runs again only when JavaScript evaluates a separate class definition again.

A practical limitation is shared mutable state. If a static field contains an object or array and a subclass inherits access to it, both classes may refer to the same object unless the subclass defines its own static value. Production code should control such mutations carefully.

Example

The example records the order in which static elements run. Base first creates its log, then its name label, then its computed name, and then its static block records that Base initialization happened. Child is evaluated afterward. Its static field and static block then run. Child inherits the Base static method named record. Calling Child.record uses Child as this, while calling Base.record uses Base as this. Because Child inherits the same log array and does not define its own log, changes made through Child are visible through Base.log as well. Constructing Base or Child instances afterward does not add static initialization entries because static elements are not part of instance construction.

Code
class Base {
  // This shared array is created while Base itself is being evaluated.
  static log = [];

  // Static fields run in source order during class evaluation.
  static nameLabel = 'Base';

  // this refers to Base while this Base static field is initialized.
  static computedName = `${this.nameLabel} class`;

  // This block runs once during this evaluation of Base.
  static {
    this.log.push(`initialized ${this.computedName}`);
  }

  // The receiver of the call decides what this means in this static method.
  static record() {
    this.log.push(`record called on ${this.nameLabel}`);
  }
}

class Child extends Base {
  // Child static initialization happens after Base already exists.
  static nameLabel = 'Child';

  // Child inherits Base.log, so this mutation reaches the same array.
  static {
    this.log.push(`initialized ${this.nameLabel} class`);
  }
}

// Base.log already contains entries from both class evaluations.
console.log(Base.log);

// Child inherits record. Because Child receives the call, this is Child.
Child.record();

// Instance construction does not repeat static initialization.
new Base();
new Child();

// Both names resolve to the same inherited log array in this example.
console.log(Base.log);
console.log(Child.log);
Where it is used

Static initialization is useful for class level configuration, registries, counters, lookup data, validation of related class settings, and setup that should happen when a class is evaluated. A frontend library might use a static block to register metadata or prepare shared values. Static fields are also useful when all instances should refer to one class level value. Shared mutable static state should be used carefully because changes can affect every caller that reaches the same inherited object.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands when class level state is created, the order in which static elements run, how static members behave with inheritance, and how this works inside static methods. It also tests whether the candidate can clearly separate class initialization from instance construction.

Common interview mistakes

A common mistake is saying static initialization runs when the first object is created. It actually runs when the class definition is evaluated. Another mistake is assuming every new instance reruns a static block. It does not. Candidates also sometimes think an inherited static method always uses the base class as this. The receiver matters, so Child.record() uses Child as this. Another mistake is forgetting that an inherited static object can still be the same object owned by the base class. Mutating that object through the subclass can therefore affect what the base class sees.

Interview tip

Start by saying that static fields and static blocks run during class evaluation, not during new. Then explain source order, base class before subclass, and the receiver rule for this in static methods. Use one small execution log to show the behavior clearly.

Interviewer may ask next
What happens if a subclass calls an inherited static method that uses this?

The subclass becomes this when the method is called through the subclass. For example, Child.record() uses Child as this even though record was declared on Base. This matters because property reads and writes through this can resolve to subclass static properties or inherited static properties. The method is inherited, but the receiver is determined by the call.

What is the main production concern with mutable static fields shared through inheritance?

The main concern is shared mutable state. If a base class owns a static array or object and a subclass only inherits access to it, both classes can refer to that same object. A mutation through the subclass can therefore be visible through the base class. This can be useful for one shared registry, but it can also create surprising coupling. If each subclass needs independent state, define a separate static value on each subclass.

39. How do `Object.is`, `===`, and SameValueZero equality differ?Language SpecificMedium

Question Details

Compare NaN, positive and negative zero, primitive strings, and object references under Object.is, strict equality, and the equality used by Set and Array.prototype.includes. Explain which relation treats NaN as equal to itself and which one distinguishes the two zero signs.

Short Interview Answer (30-60 seconds)

===, Object.is, and SameValueZero are almost the same, but NaN and signed zero show their differences. === says NaN is not equal to itself and treats positive zero and negative zero as equal. Object.is says NaN equals itself and distinguishes positive zero from negative zero. SameValueZero says NaN equals itself but treats both zero signs as equal. Set and Array.prototype.includes use SameValueZero. For objects, all three compare identity, so two separate objects with the same contents are not equal.

Detailed Explanation

See the Code while reading this explanation.

JavaScript has several rules for deciding whether two values should count as the same. Most ordinary values give the same result with all three rules, but two unusual number cases behave differently. One case is NaN, which represents a numeric result that is not a normal number. The other case is zero, because JavaScript can preserve a positive sign or a negative sign on zero. Objects are also important because JavaScript checks whether two values refer to the same object, rather than checking whether their contents merely look the same.

Useful Questions to Ask the Interviewer
  1. Would you like me to compare the important cases with a small code example?
  2. Should I also explain how Set and Array.prototype.includes use SameValueZero?
How do `Object.is`, `===`, and SameValueZero equality differ? diagram
How to Explain It in an Interview

Start with the practical rule. Use === for normal strict comparisons. Use Object.is when the exact behavior of NaN or signed zero matters. Remember that Set and Array.prototype.includes use SameValueZero.

For ordinary primitive values, all three relations usually agree. For example, the primitive strings "hello" and "hello" compare as equal with ===, Object.is, and SameValueZero.

The first important case is NaN. NaN === NaN is false. Object.is(NaN, NaN) is true. SameValueZero also treats NaN as equal to itself. This is why [NaN].includes(NaN) returns true. A Set also treats repeated NaN values as the same entry.

The second important case is signed zero. JavaScript has positive zero and negative zero. Strict equality treats them as equal, so 0 === -0 is true. SameValueZero also treats them as equal. Object.is is different because Object.is(0, -0) is false. This makes Object.is useful when the sign of zero must be preserved as part of the comparison.

For objects, all three relations compare object identity. Two separately created objects are not equal even when their properties contain the same values. A reference compared with itself is equal.

These comparisons do not copy or allocate the compared values. The important production concern is choosing the equality behavior that matches the operation. In most application logic, === is the normal choice. For collection membership with Set or includes, understand that SameValueZero makes NaN searchable and combines both zero signs.

Example

The example compares the same important values under the three equality behaviors. It shows that ordinary primitive strings agree under all three rules. It shows that NaN differs under strict equality, while signed zero differs under Object.is. It also shows that two separate objects are different, while the same object reference is equal. Set and Array.prototype.includes demonstrate SameValueZero because JavaScript does not provide SameValueZero as a standalone comparison function.

Code
const firstObject = { value: 1 };
const secondObject = { value: 1 };

// Compare NaN with the three relevant equality behaviors.
console.log('NaN with strict equality:', NaN === NaN); // false
console.log('NaN with Object.is:', Object.is(NaN, NaN)); // true
console.log('NaN with includes:', [NaN].includes(NaN)); // true

// Signed zero is equal with strict equality and SameValueZero, but not with Object.is.
console.log('Signed zero with strict equality:', 0 === -0); // true
console.log('Signed zero with Object.is:', Object.is(0, -0)); // false
console.log('Signed zero with includes:', [0].includes(-0)); // true

// Equal primitive strings represent the same primitive string value.
console.log('Strings with strict equality:', 'hello' === 'hello'); // true
console.log('Strings with Object.is:', Object.is('hello', 'hello')); // true
console.log('Strings with includes:', ['hello'].includes('hello')); // true

// Separate objects have different identities even when their contents match.
console.log('Separate objects with strict equality:', firstObject === secondObject); // false
console.log('Separate objects with Object.is:', Object.is(firstObject, secondObject)); // false
console.log('Separate objects with includes:', [firstObject].includes(secondObject)); // false

// The same object reference is equal under all three behaviors.
console.log('Same reference with strict equality:', firstObject === firstObject); // true
console.log('Same reference with Object.is:', Object.is(firstObject, firstObject)); // true
console.log('Same reference with includes:', [firstObject].includes(firstObject)); // true

// Set uses SameValueZero, so repeated NaN values become one entry and both zero signs become one entry.
const values = new Set([NaN, NaN, 0, -0]);
console.log('Set size:', values.size); // 2
Where it is used

=== is the normal choice for strict comparisons in application logic because it compares without type conversion. Object.is is useful when code must distinguish positive zero from negative zero or deliberately recognize NaN as the same value. SameValueZero is used by standard JavaScript APIs such as Set membership and Array.prototype.includes. This matters when frontend code stores unique primitive values, checks whether an array contains NaN, or reasons about collection membership. These equality checks do not copy the values or create new objects as part of the comparison.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands JavaScript equality beyond the common === operator. The important cases are NaN, positive zero, negative zero, primitive values, and object identity. It also tests whether the candidate knows that standard APIs such as Set and Array.prototype.includes use SameValueZero and can therefore behave differently from strict equality.

Common interview mistakes

A common mistake is saying that Object.is is simply a stricter version of ===. It is not. They specifically disagree for NaN and signed zero. Another mistake is assuming Array.prototype.includes uses ===. It uses SameValueZero, so [NaN].includes(NaN) is true. Developers also sometimes expect objects with identical properties to compare as equal. These equality relations compare object identity, so two separately created objects are different. Another mistake is assuming a Set can keep positive zero and negative zero as separate values. SameValueZero treats them as the same value.

Interview tip

State the two special number cases first. Say that Object.is treats NaN as equal to itself and distinguishes the two zero signs. Then say that SameValueZero also treats NaN as equal to itself but considers both zero signs equal. Finish by noting that Set and Array.prototype.includes use SameValueZero and that objects are compared by identity.

Interviewer may ask next
Why does `[NaN].includes(NaN)` return true while `[NaN].indexOf(NaN)` returns negative one?

Array.prototype.includes returns true because it uses SameValueZero, which treats NaN as equal to itself. Array.prototype.indexOf uses strict equality semantics for its element comparison, and NaN === NaN is false, so it does not find that element and returns negative one. This matters when application data can contain NaN. Use includes when the goal is a membership check and SameValueZero is the desired behavior.

When would you choose `Object.is` instead of `===` in production code?

Choose Object.is when the exact NaN or signed zero behavior matters. It treats two NaN values as the same and distinguishes positive zero from negative zero. For most ordinary application comparisons, === is simpler and more familiar. The tradeoff is semantic rather than a copying or memory concern. Object.is gives the precise edge case behavior when the application needs it, while === is usually clearer when those cases do not matter.

40. How do inherited properties become shadowed or exposed again?Language SpecificMedium

Question Details

Create a prototype with a writable data property and an accessor, then assign similarly named properties on a child object. Explain when assignment creates an own property, when an inherited setter runs, how delete reveals the inherited value again, and how Object.hasOwn verifies the result.

Short Interview Answer (30-60 seconds)

An inherited writable data property is usually shadowed when I assign the same property name on the child, because JavaScript creates an own property on the child. An inherited setter behaves differently. Assignment calls the setter and does not automatically create an own property with that name. If I delete a shadowing own property, normal lookup can see the inherited property again. I can use Object.hasOwn to confirm whether the property belongs directly to the child.

Detailed Explanation

See the Code while reading this explanation.

A child object can use values that come from another object above it. If the child gets its own value with the same name, that value can hide the older one. Removing the child value can make the older value visible again. There is one important special case. If the older object reacts when a value is assigned, the assignment can trigger that reaction instead of storing a new value on the child. We should check which object really owns each value after every change.

Useful Questions to Ask the Interviewer
  1. Should I show both a normal stored value and a property that reacts to assignment?
  2. Should I verify ownership with Object.hasOwn after assignment and deletion?
How do inherited properties become shadowed or exposed again? diagram
How to Explain It in an Interview

Suppose proto has a writable data property named score with value 10. A child object inherits from proto. Reading child.score first returns 10 because child has no own score, so property lookup continues to proto.

When we run child.score = 20, JavaScript finds the inherited data property and sees that it is writable. The assignment creates an own score property on child with value 20. That own property now shadows the inherited score. The original proto.score is still 10. Object.hasOwn(child, "score") returns true.

Now consider an inherited accessor named status that has a setter. When we assign child.status = "ready", JavaScript calls the inherited setter with child as this. Assignment does not automatically create an own status property. In this example, the setter stores the received value in child.savedStatus. Object.hasOwn(child, "status") therefore remains false.

If we delete child.score, only the own score property is removed. The prototype property is not deleted. Reading child.score again continues through the prototype chain, finds proto.score, and returns 10. Object.hasOwn(child, "score") now returns false.

This behavior matters when objects inherit shared defaults or shared behavior. Shadowing is useful when one object needs its own value. An inherited setter can validate, transform, or redirect an assignment. A key limitation is that an inherited data property that is not writable blocks normal assignment to that name. Another important point is that delete only removes a configurable own property from the object where delete is used. Prototype based designs can save repeated definitions, but they can also make ownership less obvious, so production code should use clear conventions and ownership checks when that distinction matters.

Example

The example creates proto with two inherited properties. score is a writable data property. status is an accessor with a setter and getter. child inherits from proto. Assigning child.score creates an own score property that shadows proto.score. Assigning child.status calls the inherited setter with child as this. The setter stores the value in child.savedStatus, so child does not gain an own status property. Deleting child.score removes the own shadowing property, so reading child.score finds the inherited value 10 again. Object.hasOwn verifies whether score and status belong directly to child.

Code
const proto = {};

// Define a writable data property that a child can shadow with its own value.
Object.defineProperty(proto, 'score', {
  value: 10,
  writable: true,
  enumerable: true,
  configurable: true,
});

// Define an inherited accessor whose setter redirects the assigned value.
Object.defineProperty(proto, 'status', {
  get() {
    return this.savedStatus ?? 'unknown';
  },
  set(value) {
    // Store the value on the receiving child instead of creating its own status property.
    this.savedStatus = value;
  },
  enumerable: true,
  configurable: true,
});

// Create a child whose property lookup can continue to proto.
const child = Object.create(proto);

console.log(child.score); // 10
console.log(Object.hasOwn(child, 'score')); // false

// This assignment creates an own score because the inherited data property is writable.
child.score = 20;
console.log(child.score); // 20
console.log(proto.score); // 10
console.log(Object.hasOwn(child, 'score')); // true

// The inherited setter runs with child as this and stores the value in savedStatus.
child.status = 'ready';
console.log(child.status); // ready
console.log(child.savedStatus); // ready
console.log(Object.hasOwn(child, 'status')); // false

// Delete only the own shadowing score so lookup exposes proto.score again.
delete child.score;
console.log(child.score); // 10
console.log(Object.hasOwn(child, 'score')); // false
Where it is used

This behavior appears when applications use prototypes for shared defaults or shared behavior and allow individual objects to override selected values. It also matters with JavaScript classes because methods and accessors declared in a class are normally placed on the class prototype. Understanding shadowing helps when debugging configuration objects, model objects, custom component state, inherited getters and setters, and code that removes temporary overrides so lookup returns to a shared default.

Why Interviewers Ask This

Interviewers ask this to check whether a candidate understands how property assignment works with prototypes, data properties, setters, deletion, and own property checks. The question also tests whether the candidate can predict runtime behavior instead of assuming every assignment creates a new property on the child object.

Common interview mistakes

A common mistake is assuming every assignment creates an own property. An inherited setter can receive the assignment instead. Another mistake is thinking that shadowing changes the prototype property. It does not. The own property only hides it during lookup. Developers may also expect delete child.score to delete proto.score, but delete only targets an own property on child. Another mistake is using the in operator as an ownership check because in reports both own and inherited properties. Object.hasOwn checks direct ownership. It is also incorrect to assume normal assignment can shadow an inherited data property that is not writable.

Interview tip

Explain the two assignment cases separately. First show a writable inherited data property that becomes shadowed by an own property. Then show an inherited setter that receives the assignment without automatically creating that property on the child. Finish by deleting the shadowing property and using Object.hasOwn to prove that the inherited value is visible again.

Interviewer may ask next
What happens if the inherited data property is not writable?

Normal assignment does not create a shadowing own property when an inherited data property with that name is not writable. In strict mode, the assignment throws a TypeError. Outside strict mode, the assignment fails without changing the value. This matters because the inherited property descriptor can block an override through ordinary assignment. If the child is extensible, Object.defineProperty can still define a new own property directly because defining an own property follows different rules from ordinary assignment.

What is the production tradeoff of relying on prototype shadowing for object specific values?

Prototype shadowing can reduce duplication for shared defaults while allowing each child to store only the values it overrides. The tradeoff is that property ownership becomes less obvious because a visible value may come from the child or from its prototype. This can make debugging, serialization, and maintenance harder. In production code, Object.hasOwn and clear property conventions help distinguish direct object state from inherited defaults.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.