10 Javascript Interview Questions — part 1
While AI is great, and we use it a lot while coding, we still have to know JavaScript to clear JS interviews. We need to know the fundamentals and advanced concepts. In this article series, we’ll go through interesting JS questions that will test your knowledge of the language. Let’s get started.
1 What happens when you call setTimeout with a delay of 0, and why might the actual execution time vary?
When you call setTimeout(callback, 0), the callback function is not executed immediately. Instead, it's placed at the back of the message queue. The JavaScript engine will only execute this callback once the current call stack is empty. This is why the actual execution time may vary—it depends on the length of time it takes to process all the code currently in the call stack before the event loop can pull the callback from the message queue.
Here’s a simple example to illustrate this behavior:
console.log('Start');
setTimeout(() => {
console.log('Callback executed');
}, 0);
console.log('End');
// The expected output will be:
// Start
// End
// Callback executedWhy the Actual Execution Time Varies
The delay isn’t a guaranteed execution time, but a minimum delay. Several factors can influence the actual time of execution:
- Call Stack: If the current code in the call stack is computationally expensive or takes a long time to run, the
setTimeoutcallback will have to wait for it to finish. The callback only gets its turn when the stack is clear. - Event Loop: The event loop is what checks the call stack and the message queue. It continuously pushes new tasks from the message queue to the call stack when the stack is empty.
- Browser/Node.js Environment: The JavaScript engine’s internal workings, including other tasks in the message queue (like user interactions or network requests), can affect the order and timing of execution.
This mechanism ensures that JavaScript, being a single-threaded language, doesn’t block the main thread. It allows for asynchronous operations without freezing the user interface.
2 Explain the difference between Object.create(null) and {}, and when you would use each.
The primary difference between Object.create(null) and {} lies in their prototype chain.
{}is a shorthand fornew Object(). It creates a plain object that inherits fromObject.prototype. This means it has access to common methods and properties liketoString(),hasOwnProperty(), andisPrototypeOf().Object.create(null)creates an object with no prototype at all. It's a truly empty object, often referred to as a "dictionary" or "hash map," with no inherited properties or methods fromObject.prototype.
This distinction can be seen in the following example:
const obj1 = {};
const obj2 = Object.create(null);
console.log(obj1.toString); // function toString() { ... }
console.log(obj2.toString); // undefined
console.log(obj1.hasOwnProperty('a')); // false
// console.log(obj2.hasOwnProperty('a')); // TypeError: obj2.hasOwnProperty is not a function
console.log(Object.getPrototypeOf(obj1)); // [Object: null prototype] {}
console.log(Object.getPrototypeOf(obj2)); // nullWhen to Use Each
Use {} when:
You need a standard object for general-purpose use. This is the most common use case for objects in JavaScript. Since it inherits from Object.prototype, you can use built-in methods like toString(), hasOwnProperty(), and others without any special checks.
Use Object.create(null) when:
- You need a pure data map: Use this when you want to create an object that will only serve as a hash map or dictionary. It’s safe from prototype pollution, which is a common security vulnerability.
- You’re using the object to store user-provided keys: A malicious user could provide a key like
"toString", which would overwrite the inherited method in a regular object ({}) but won't affect an object created withObject.create(null). - Performance is critical: An object with no prototype chain can be slightly faster for property lookups and access, as the JavaScript engine doesn’t have to traverse the prototype chain.
- You’re building an API or a library: When building an API or a library that exposes objects, using
Object.create(null)ensures that the consumers of your API don't accidentally rely on inherited methods that you haven't explicitly documented or intended to be part of the object's public API.
3 What are the potential issues with this code, and how would you fix them?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}The code will print 3 three times instead of 0, 1, and 2 as one might expect. This happens because of a classic JavaScript problem involving var and closures within a loop.
The Issue
The core issue is that var has function scope, not block scope. The variable i is declared once and is hoisted to the top of the function or global scope. The setTimeout callback function, which executes asynchronously after the loop has finished, forms a closure over the variable i. By the time the callbacks are executed, the loop has already completed, and the value of i has been incremented to 3. All three callbacks reference the same i variable in the outer scope, which holds the final value of 3.
Solutions
There are three common ways to fix this issue, each utilizing a different approach to create a new, distinct scope for each iteration.
1. Using let
The most modern and idiomatic solution is to use the let keyword, which has block scope. A new i is created for each iteration of the loop, so each setTimeout callback captures the correct value.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Expected Output: 0, 1, 22. Using an Immediately Invoked Function Expression (IIFE)
Before let was widely adopted, a common solution was to wrap the setTimeout call in an IIFE. The IIFE creates a new function scope for each iteration, and by passing i as an argument, you can capture its value. This is a functional programming approach to creating a closure.
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 100);
})(i);
}
// Expected Output: 0, 1, 23. Using bind()
Another less common but valid solution is to use bind(). The first argument to bind() is the value for this, which is not relevant here, so we pass null. The second argument allows you to pass a value to be used as an argument to the function when it's executed, effectively creating a new scope for each call.
for (var i = 0; i < 3; i++) {
setTimeout(console.log.bind(null, i), 100);
}
// Expected Output: 0, 1, 24 How does JavaScript’s garbage collection work, and what can cause memory leaks in modern applications?
JavaScript’s garbage collection is primarily handled by an algorithm called mark-and-sweep. The garbage collector’s job is to reclaim memory that’s no longer in use by the program.
The Mark-and-Sweep Algorithm
- Marking Phase: The garbage collector starts from the root objects, which are variables in the global scope (like
windoworglobal), and objects on the current call stack. It then "marks" all objects that are reachable from these roots. This includes following references from the root objects to other objects, and then from those objects to others, and so on. Any object that can be reached from the root is considered "in use." - Sweeping Phase: The garbage collector then “sweeps” through all memory. Any objects that were not marked as reachable are considered “garbage” and are removed, and their memory is freed.
Common Causes of Memory Leaks
Although JavaScript’s automatic garbage collection prevents many memory leaks, some common patterns in modern applications can still cause them. These leaks occur when objects that are no longer needed are still being referenced, preventing the garbage collector from freeing their memory.
- Global Variables: Declaring variables without a keyword (
var,let, orconst) inside a function will make them global. They will then be referenced by the global object and will not be garbage collected until the application is closed.
function createLeak() {
// 'leaky' becomes a global variable (e.g., window.leaky in a browser)
leaky = 'I am leaking!';
}2. Forgotten Timers and Event Listeners: If you set a setTimeout, setInterval, or an event listener without clearing it, the callback function will hold a reference to the objects within its scope. This prevents those objects from being garbage collected, even if the element or component they are associated with is removed from the DOM.
const element = document.getElementById('my-button');
element.addEventListener('click', () => {
// This listener will not be garbage collected until the page unloads,
// even if `element` is removed from the DOM.
});
// To prevent the leak, you must remove the listener
// element.removeEventListener('click', ...);3. Detached DOM Elements: If you remove a DOM element from the document but a JavaScript variable still holds a reference to it, the entire element and its data will remain in memory. The garbage collector sees this as a valid reference and won’t free the memory.
let myElement = document.getElementById('some-id');
document.body.removeChild(myElement); // The element is removed from the DOM.
// However, `myElement` variable still holds a reference, causing a leak.
myElement = null; // Setting the reference to null prevents the leak.4. Improper Closures: While closures are a powerful feature, they can cause memory leaks if not used carefully. A closure that references a large object or an element from an outer scope will keep that object in memory as long as the closure exists. If that closure is never released, the referenced object can never be collected.
5 Explain the differences between call, apply, and bind, then implement your own version of bind.
call, apply, and bind are all methods used to explicitly set the this context for a function. The main difference lies in how they handle function execution and arguments.
call
- Executes the function immediately.
- The first argument is the
thisvalue. - The subsequent arguments are passed to the function individually, one by one.
Syntax: function.call(thisArg, arg1, arg2, ...)
const person = { name: 'Alice' };
function greet(city, country) {
console.log(`Hello, my name is ${this.name} and I live in ${city}, ${country}.`);
}
greet.call(person, 'New York', 'USA');
// Output: Hello, my name is Alice and I live in New York, USA.apply
- Executes the function immediately.
- The first argument is the
thisvalue. - The arguments are passed to the function as a single array or an array-like object.
Syntax: function.apply(thisArg, [argsArray])
const person = { name: 'Bob' };
function greet(city, country) {
console.log(`Hello, my name is ${this.name} and I live in ${city}, ${country}.`);
}
greet.apply(person, ['London', 'UK']);
// Output: Hello, my name is Bob and I live in London, UK.bind
- Returns a new function with the
thiscontext permanently bound. - Does not execute the function immediately.
- Arguments can be passed to the returned function at a later time.
Syntax: const newFunction = function.bind(thisArg, arg1, arg2, ...)
const person = { name: 'Charlie' };
function greet() {
console.log(`Hello, my name is ${this.name}.`);
}
const boundGreet = greet.bind(person);
boundGreet();
// Output: Hello, my name is Charlie.The main takeaway is: call and apply are for immediate execution, while bind is for creating a new, reusable function. The only difference between call and apply is how they accept arguments.
Implementing Your Own bind
To create a custom bind implementation, we need a function that returns another function. This returned function should be able to accept its own arguments and combine them with any arguments passed to the original bind call. We'll use a Symbol for the method name to avoid potential conflicts with existing properties on Function.prototype.
Function.prototype.myBind = function(thisArg, ...args) {
const func = this; // 'this' refers to the function on which myBind is called.
return function(...restArgs) {
// Combine arguments from myBind and the returned function.
const allArgs = [...args, ...restArgs];
// Use apply to call the original function with the correct context and arguments.
return func.apply(thisArg, allArgs);
};
};
// Example usage with our custom myBind
const person = { name: 'Dave' };
function greet(city, country) {
console.log(`Hello, my name is ${this.name} and I live in ${city}, ${country}.`);
}
const boundGreet = greet.myBind(person, 'Tokyo');
boundGreet('Japan');
// Output: Hello, my name is Dave and I live in Tokyo, Japan.6 What is the temporal dead zone, and how does it affect let, const, and var declarations?
The Temporal Dead Zone (TDZ) is a behavioral characteristic of let and const declarations. It's the period between the beginning of a block scope and the point where a variable is declared and initialized with a value. During this time, the variable exists but cannot be accessed. Any attempt to access it will result in a ReferenceError.
How It Affects Declarations
let and const
Both let and const declarations are hoisted, but they are not initialized. This creates the TDZ. The variable name is "hoisted" to the top of its block scope, but it remains in a state where it cannot be accessed until the code execution reaches its actual declaration line.
Example:
{
// TDZ for `x` starts here
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 'hello'; // TDZ for `x` ends here
}This behavior is a key feature that helps catch common programming errors and promotes better code practices by making variable declaration explicit.
var
The var keyword does not have a temporal dead zone. Variables declared with var are fully hoisted—both their declaration and an initialization to undefined are moved to the top of their function or global scope. This means you can access a var variable before its declaration line without getting a ReferenceError.
{
console.log(y); // undefined
var y = 'world';
console.log(y); // world
}While this might seem flexible, it can lead to unexpected behavior and subtle bugs, which is one of the main reasons let and const were introduced to the language. The TDZ for let and const provides more predictable and safer variable scoping.
7 How would you implement deep equality comparison for two JavaScript objects, handling edge cases like circular references?
Deep equality comparison for two JavaScript objects is the process of checking if their properties and values are strictly equal, recursively. This goes beyond a simple === check, which only compares object references. A robust implementation needs to handle various data types and, importantly, circular references to prevent infinite loops.
Implementation without Circular Reference Handling
A basic recursive function can handle most cases. It would check for:
- Simple data types: Use strict equality (
===) for primitives. - Object and array types: Recursively call the function on their properties and elements.
- Null and other edge cases: Handle
nulland non-object types appropriately.
Here’s a simple example that works for non-circular objects:
function isEqual(obj1, obj2) {
// 1. Check for strict equality (handles primitives and same object reference)
if (obj1 === obj2) {
return true;
}
// 2. Check for null or non-object types
if (obj1 === null || typeof obj1 !== 'object' || obj2 === null || typeof obj2 !== 'object') {
return false;
}
// 3. Get keys and check if they have the same number of properties
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
// 4. Recursively check properties
for (const key of keys1) {
if (!keys2.includes(key) || !isEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}This implementation works well, but it will cause a stack overflow error if it encounters a circular reference.
Implementing Deep Equality with Circular Reference Handling
To handle circular references, we need to keep track of the objects we have already visited during the recursion. A good way to do this is with a WeakMap or two arrays to store the objects from each comparison pair. When comparing two objects, we first check if they have already been seen together in the current comparison chain.
Here is a more robust implementation using a WeakMap to store object pairs that have been visited.
function isEqualCircular(obj1, obj2, map = new WeakMap()) {
// 1. Strict equality check for primitives and same object reference
if (obj1 === obj2) {
return true;
}
// 2. Handle null or non-object types
if (obj1 === null || typeof obj1 !== 'object' || obj2 === null || typeof obj2 !== 'object') {
return false;
}
// 3. Check for circular references
// If we've already compared these two objects, return true to prevent an infinite loop.
// We use a WeakMap to avoid memory leaks.
if (map.get(obj1) === obj2) {
return true;
}
map.set(obj1, obj2); // Store the current pair of objects
// 4. Handle array and object types
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) {
return false;
}
// 5. Recursively check properties
for (const key of keys1) {
// We need to check if key exists in obj2 to avoid potential errors
if (!Object.prototype.hasOwnProperty.call(obj2, key) || !isEqualCircular(obj1[key], obj2[key], map)) {
return false;
}
}
return true;
}
// Example with a circular reference
const a = { b: null };
const b = { a: a };
a.b = b;
const c = { d: null };
const d = { c: c };
c.d = d;
console.log(isEqualCircular(a, c)); // true8 What are the performance implications of different iteration methods (for, forEach, map, for…of) and when would you choose each?
Different iteration methods in JavaScript have varying performance characteristics, with for loops generally being the fastest and most flexible, while array-specific methods like forEach and map provide better readability and functional benefits at a small performance cost.
Performance Implications
forloop: This is the fastest iteration method. It's a low-level construct with minimal overhead. It has direct control over the loop index, making it memory-efficient and ideal for large datasets or performance-critical tasks.for...ofloop: This loop is designed to iterate over iterable objects, such as arrays, strings, and maps. It is generally faster thanforEachandmapbecause it doesn't involve a function call for each iteration. It provides a clean syntax and avoids common off-by-one errors associated withforloops.forEach: This is an array method that calls a callback function for each element. The performance overhead comes from the function call in each iteration. While this overhead is negligible for small arrays, it can accumulate for large datasets. It also cannot be stopped or broken usingbreakorreturn, which can be a limitation.map: Similar toforEach,mapalso involves a function call for each element, incurring a similar performance cost. However, its main distinction is that it returns a new array with the results of the callback function. This can be less performant thanforloops if you don't need to create a new array.
When to Choose Each
forloop: Choose aforloop when performance is the top priority, especially with large datasets, or when you need fine-grained control over the iteration, such as skipping elements, breaking out of the loop, or iterating in reverse.for...ofloop: Usefor...ofwhen you want a simple and readable way to iterate over the values of an array or iterable object and do not need the index. It's a great balance of performance and readability.forEach: This is the go-to method for iterating over an array when readability is more important than absolute performance, and you need to perform a side effect for each element (e.g., logging to the console, pushing to another array).map: Choosemapspecifically when you need to transform each element of an array into a new value and create a new array containing the results. It's a core concept of functional programming and makes your code declarative and concise.
9 Explain how JavaScript’s this binding works in arrow functions vs regular functions, especially in the context of event handlers and method calls.
In JavaScript, the behavior of this is a crucial difference between regular functions and arrow functions. The key is that a regular function's this is dynamic and determined by how the function is called, while an arrow function's this is lexically scoped, meaning it's determined by the surrounding code where it was defined.
Regular Functions
The this value in a regular function is determined at runtime by its invocation context. It can change depending on how the function is called.
- Method Call: When a function is called as a method of an object (e.g.,
obj.myMethod()),thisrefers to the object itself. - Simple Function Call: In a standard function call (e.g.,
myFunction()),thisrefers to the global object in non-strict mode (likewindowin browsers) and isundefinedin strict mode. - Constructor Call: When a function is called with
new(e.g.,new MyObject()),thisrefers to the newly created instance. - Event Handlers: In the context of a DOM event listener,
thistypically refers to the element that the event listener was attached to.
Example:
const person = {
name: 'Alice',
greet: function() {
console.log(this.name); // 'this' refers to the 'person' object
}
};
person.greet(); // Output: Alice
const greetFunc = person.greet;
greetFunc(); // Output: '' (or 'undefined' in strict mode)In the second call, the function is detached from the person object, and its this context defaults to the global object.
Arrow Functions
Arrow functions handle this differently. They do not have their own this binding. Instead, they lexically inherit the this value from the enclosing scope at the time they are defined. This behavior is fixed and cannot be changed by call, apply, or bind.
- In a Global Scope:
thisis the global object. - Inside a Regular Function:
thisinherits thethisof the regular function. - Inside a Method:
thisinherits thethisof the enclosing object, which is the object the method is defined on.
This makes them predictable and very useful for callbacks, especially in event handlers, where a regular function’s this might not be what you expect.
Example:
const person = {
name: 'Bob',
greet: function() {
const innerArrowFunc = () => {
console.log(this.name); // 'this' inherits from the outer 'greet' function
};
innerArrowFunc();
},
arrowGreet: () => {
console.log(this.name); // 'this' inherits from the global scope (e.g., 'window')
}
};
person.greet(); // Output: Bob
person.arrowGreet(); // Output: '' (or 'undefined')Event Handlers
This distinction is especially important in event handling.
- Regular function handler:
thiswill refer to the DOM element that triggered the event. This is useful if you need to manipulate the element directly. - Arrow function handler:
thiswill refer to the context where the handler was defined, which is often the component instance in frameworks like React or Vue, making it a common pattern.
const button = document.querySelector('#myButton');
// Regular function
button.addEventListener('click', function() {
console.log(this); // 'this' is the button element
});
// Arrow function
button.addEventListener('click', (event) => {
// In a component class, 'this' would refer to the class instance
// Here, it would be the global object (window)
console.log(this);
});10 What happens during the execution of this code, and what are the potential race conditions?
Promise.resolve().then(() => console.log('A'));
setTimeout(() => console.log('B'), 0);
console.log('C');During the execution of this code, the output will be C, A, B. This is a classic example that demonstrates the difference between the microtask queue (used by promises) and the macrotask queue (used by setTimeout).
Execution Breakdown
- Synchronous Code: The JavaScript engine first executes all synchronous code.
console.log('C')is called immediately. - Promise Microtask:
Promise.resolve().then(...)places its callback (() => console.log('A')) into the microtask queue. The microtask queue has higher priority than the macrotask queue and is processed as soon as the current call stack is empty. - setTimeout Macrotask:
setTimeout(() => console.log('B'), 0)places its callback (() => console.log('B')) into the macrotask queue. Macrotasks are processed by the event loop only after the microtask queue is empty. - Event Loop: Once the synchronous code is finished, the event loop checks the microtask queue. It finds the callback for
console.log('A'), moves it to the call stack, and executes it. - Final Execution: After the microtask queue is empty, the event loop then checks the macrotask queue. It finds the callback for
console.log('B'), moves it to the call stack, and executes it.
This priority system ensures that all promises are resolved before the next event loop cycle begins.
Potential Race Conditions
Race conditions can occur when multiple asynchronous tasks are initiated, and their execution order is not guaranteed, leading to unpredictable outcomes. In this specific code, the setTimeout callback has a defined minimum delay of 0, but its actual execution time depends on several factors:
- Task Queue Congestion: If there are other tasks (e.g., from other
setTimeoutcalls, user events, or network requests) already in the macrotask queue,console.log('B')will have to wait for them to finish. - Microtask Overload: If the microtask queue has many
PromiseorqueueMicrotaskcallbacks, thesetTimeoutcallback will be delayed until all microtasks are processed, making its execution time highly variable.
These race conditions highlight that setTimeout(..., 0) is not a zero-delay operation but rather a scheduling mechanism that yields control back to the event loop, with the exact timing depending on the engine's current workload.
Thanks for reading!

