Why does typeof null return "object"? Why does 0.1 + 0.2 not equal 0.3? Why does a simple for-loop with setTimeout print the same number five times? These aren't obscure edge cases—they're deliberate interview questions designed to expose whether you've truly wrestled with JavaScript's quirks or just memorized syntax. This guide covers the tricky questions that separate candidates who understand JavaScript from those who've only used it.
Table of Contents
- Variable Scope Questions
- Closure Questions
- this Keyword Questions
- Type Coercion Questions
- Event Loop Questions
- Number and Array Questions
- Prototype Questions
- Quick Reference
Variable Scope Questions
These questions test your understanding of how JavaScript handles variable declarations and hoisting.
What happens with var x = y = 5 inside an IIFE?
This question looks innocent but exposes a fundamental misunderstanding about variable declaration:
(function () {
var x = y = 5;
})();
console.log(typeof x);
console.log(typeof y);The output is undefined for x and number for y. That line var x = y = 5 looks like it's declaring two variables, but JavaScript interprets this as:
y = 5; // Assignment without declaration - creates global!
var x = y; // Local variable declaration and assignmentThe var keyword only applies to x. The y is assigned without any declaration keyword, which in non-strict mode means JavaScript creates it as a property on the global object. After the IIFE executes, x is gone (it was function-scoped), but y is sitting there on the global object with the value 5.
This is why strict mode exists. If you add "use strict"; at the top, this code throws a ReferenceError because assigning to an undeclared variable becomes an error.
Why does this hoisting example output 99?
Hoisting confuses developers at every level. Consider this code:
var price = 99;
function calculatePrice() {
price = 100;
return;
function price() {}
}
calculatePrice();
console.log(price);The output is 99, not 100. Here's what JavaScript actually sees after hoisting:
var price = 99;
function calculatePrice() {
// Hoisted function declaration creates a local 'price' variable
function price() {}
price = 100; // Modifies the LOCAL price, not the global one
return;
}
calculatePrice();
console.log(price); // Global price is unchanged: 99The function declaration function price() {} gets hoisted to the top of calculatePrice, creating a local variable named price that shadows the global one. When we assign 100 to price, we're modifying this local variable, leaving the global price at 99.
Why does this variable declaration log undefined?
Here's another hoisting puzzle that trips people up:
var price = 10;
var getCoursePrice = function () {
console.log(price);
var price = 100;
};
getCoursePrice();The output is undefined. Even though there's a global price set to 10, the local var price = 100 declaration is hoisted to the top of the function (but not its initialization). The function sees a local price that exists but hasn't been assigned yet.
This is called the Temporal Dead Zone with var—the variable exists but is undefined until the assignment line executes.
Closure Questions
Closures are fundamental to JavaScript. These questions test whether you understand how they capture variables.
Why does the classic for-loop with setTimeout print 5 five times?
This is perhaps the most famous JavaScript interview question of all time:
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), i * 1000);
}The output is 5, 5, 5, 5, 5—each printed roughly one second apart.
Think of closures like a photograph that captures references, not values. When each setTimeout callback is created, it doesn't take a snapshot of i's current value. Instead, it holds a reference to the same variable i that the loop is modifying. By the time any callback actually executes (even the one with 0ms delay, since the call stack must clear first), the loop has finished and i has been incremented to 5.
How do you fix the closure loop problem?
The fix with let is elegant because let creates block scope. Each iteration of the loop gets its own distinct i variable:
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), i * 1000);
}
// Output: 0, 1, 2, 3, 4Before ES6 introduced let, developers solved this with an IIFE that captured the value in a new scope:
for (var i = 0; i < 5; i++) {
(function(capturedI) {
setTimeout(() => console.log(capturedI), capturedI * 1000);
})(i);
}The key insight is that var is function-scoped while let is block-scoped, and closures capture references to variables, not their values at the time of creation.
this Keyword Questions
The this keyword in JavaScript is notoriously context-dependent. These questions expose whether you really understand how it works.
Why does a detached method call return undefined?
Consider this code:
const auth = {
accessToken: 'secret-token',
getSecretToken: function () {
return this.accessToken;
}
};
const stealToken = auth.getSecretToken;
console.log(stealToken());
console.log(auth.getSecretToken());The output is undefined then secret-token.
Here's the mental model: this in JavaScript is determined by how a function is called, not where it's defined. Think of the dot before a method call as pointing this at the object on the left.
When you call auth.getSecretToken(), there's a dot before getSecretToken, so this points to auth. But when you assign the function to stealToken and call it as stealToken(), there's no dot—it's just a standalone function call. In non-strict mode, this defaults to the global object (which doesn't have an accessToken property), giving you undefined.
How do you fix the detached method problem?
The fix using bind explicitly locks in the this value:
const stealToken = auth.getSecretToken.bind(auth);
console.log(stealToken()); // 'secret-token'Arrow functions offer another solution because they inherit this from their enclosing scope:
const auth = {
accessToken: 'secret-token',
getSecretToken: () => this.accessToken // Caution! 'this' is now outer scope
};But be careful—this can backfire if the enclosing scope isn't what you expect.
What does this output inside an IIFE?
This question combines closures, this binding, and IIFE patterns:
const course = {
title: "react",
printDetails: function () {
const self = this;
console.log("a = " + this.title);
console.log("b = " + self.title);
(function () {
console.log("c = " + this.title);
console.log("d = " + self.title);
}());
}
};
course.printDetails();The output is:
a = react
b = react
c = undefined
d = react
The self = this pattern is an old-school solution to the "disappearing this" problem. Inside printDetails, both this and self refer to course. But inside the IIFE, this is no longer bound to course—it's the global object (or undefined in strict mode).
However, self is just a regular variable that closes over the outer scope, so it still holds a reference to the course object. That's why d correctly logs "react" while c is undefined.
Modern JavaScript solves this with arrow functions:
const course = {
title: "react",
printDetails: function () {
console.log("a = " + this.title);
(() => {
console.log("c = " + this.title); // Arrow function inherits 'this'
})();
}
};Type Coercion Questions
JavaScript's type coercion can produce surprising results. These questions test whether you understand the rules.
What are the outputs of these type coercion expressions?
console.log('false' == false);
console.log(2 + true);
console.log('6' + 9);
console.log('6' - 9);
console.log(1 + 2 + "3");
console.log(2 in [1, 2]);The outputs are:
false // String 'false' is not equal to boolean false
3 // true is coerced to 1
'69' // + with a string concatenates
-3 // - with a string converts to number
'33' // 1 + 2 = 3, then 3 + "3" = "33"
false // 2 is not an index in [1, 2] (indices are 0 and 1)The 'false' == false comparison is tricky. You might think "the string 'false' represents false, so they're equal." But JavaScript doesn't work that way. With ==, the operands are converted for comparison. false becomes 0, and 'false' becomes NaN (because it's not a numeric string). 0 != NaN, so the result is false.
The + operator has dual behavior: it adds numbers but concatenates strings. When one operand is a string, the other is converted to a string. That's why '6' + 9 gives '69' but '6' - 9 gives -3 (the - operator only works with numbers, so '6' is converted to 6).
The in operator checks for property names (including array indices), not values. The array [1, 2] has indices 0 and 1, so 2 in [1, 2] is false.
Why does typeof null return 'object'?
This question tests your knowledge of JavaScript's historical quirks:
console.log(typeof null);
console.log(typeof undefined);
console.log(null === undefined);
console.log(null == undefined);The output is:
object
undefined
false
true
The typeof null === "object" result is one of JavaScript's most famous bugs. In the original implementation, values were stored with a type tag. The tag for objects was 000, and null was represented as the null pointer (0x00), which meant its type tag was also 000. So typeof incorrectly reports it as an object.
This was recognized as a bug early on, but fixing it would have broken too much existing code. A proposal to fix it was rejected because it would have been a breaking change.
The difference between null === undefined (false) and null == undefined (true) is important. With strict equality, they're different types. With loose equality, JavaScript considers them "loosely equal" because both represent "absence of value."
Why does 1 < 2 < 3 return true but 3 > 2 > 1 return false?
This question looks like simple math but exposes a JavaScript quirk:
console.log(1 < 2 < 3);
console.log(3 > 2 > 1);The output is true then false.
JavaScript doesn't support chained comparisons like Python does. Instead, it evaluates left to right with type coercion.
For 1 < 2 < 3:
1 < 2evaluates totruetrue < 3→1 < 3(true is coerced to 1) →true
For 3 > 2 > 1:
3 > 2evaluates totruetrue > 1→1 > 1(true is coerced to 1) →false
If you actually want to check if a value is between two bounds:
const x = 2;
console.log(1 < x && x < 3); // true - proper range checkEvent Loop Questions
These questions test your understanding of the event loop, which is essential for writing and debugging asynchronous JavaScript.
What is the execution order of this setTimeout code?
(function () {
console.log(1);
setTimeout(() => console.log(2), 1000);
setTimeout(() => console.log(3), 0);
console.log(4);
})();The output is 1, 4, 3, 2.
The key insight is that setTimeout(..., 0) doesn't mean "execute immediately." It means "execute as soon as possible, after the current call stack is empty."
Here's the execution flow:
console.log(1)executes synchronously. Output:1setTimeout(..., 1000)schedules a callback for ~1 second latersetTimeout(..., 0)schedules a callback for the next tick of the event loopconsole.log(4)executes synchronously. Output:4- Call stack is now empty. Event loop checks the task queue
- The 0ms timeout callback runs. Output:
3 - After ~1 second, the 1000ms callback runs. Output:
2
Why do Promises execute before setTimeout even with 0 delay?
Here's a more advanced version:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');The output is 1, 4, 3, 2.
The Promise callback (3) runs before the setTimeout callback (2) even though both are asynchronous. This is because Promise callbacks go into the microtask queue, which has higher priority than the macrotask queue (where setTimeout callbacks live).
The event loop always drains the entire microtask queue before processing the next macrotask. This means Promises, queueMicrotask, and MutationObserver callbacks always run before setTimeout, setInterval, and I/O callbacks.
Number and Array Questions
These questions test your understanding of JavaScript's number representation and array internals.
Why does 0.1 + 0.2 not equal 0.3?
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);The output is 0.30000000000000004 and false.
JavaScript uses IEEE 754 double-precision floating-point format for all numbers. The problem is that certain decimal fractions, like 0.1 and 0.2, cannot be represented exactly in binary. It's similar to how 1/3 can't be represented exactly in decimal (0.333...).
When you add two slightly imprecise numbers, the small errors accumulate, giving you 0.30000000000000004 instead of 0.3.
The practical solution is to compare floating-point numbers using a tolerance:
function areEqual(num1, num2) {
return Math.abs(num1 - num2) < Number.EPSILON;
}
console.log(areEqual(0.1 + 0.2, 0.3)); // trueFor financial calculations, work in cents (integers) and only convert to dollars for display:
const priceInCents = 1099; // $10.99
const taxInCents = 88; // $0.88
const total = (priceInCents + taxInCents) / 100; // $11.87, no precision issuesWhat is the difference between empty slots and undefined in arrays?
This question reveals understanding of how arrays actually work in JavaScript:
const courses = ['React'];
courses[5] = 'Angular';
courses[4] = undefined;
console.log(courses);
console.log(courses[3]);
console.log(courses.map(c => 'Vue'));The output is:
['React', <3 empty items>, undefined, 'Angular']
undefined
['Vue', <3 empty items>, 'Vue', 'Vue']
There's a crucial difference between an "empty slot" and a slot containing undefined. When you set courses[5] = 'Angular' without setting indices 1-4, those positions are empty slots (also called "holes"). They're not undefined—they literally don't exist.
When you access courses[3], JavaScript returns undefined because there's nothing there. But here's the key insight: array methods like map, filter, and forEach skip over empty slots entirely. They only iterate over slots that actually have values (including undefined if explicitly set).
That's why courses[4] (which we explicitly set to undefined) gets mapped to 'Vue', but indices 1-3 remain empty in the result.
You can check for empty slots using:
console.log(3 in courses); // false (empty slot)
console.log(4 in courses); // true (has value: undefined)Prototype Questions
These questions test understanding of prototypal inheritance.
What happens when you delete a property that shadows a prototype property?
const reactCourse = {
price: 59.99,
getPrice: function () {
return this.price;
}
};
const vueCourse = Object.create(reactCourse);
vueCourse.price = 69.99;
delete vueCourse.price;
console.log(vueCourse.getPrice());The output is 59.99.
Here's what's happening step by step:
Object.create(reactCourse)creates a new object withreactCourseas its prototypevueCourse.price = 69.99creates an "own property" onvueCourse, shadowing the inherited onedelete vueCourse.priceremoves only the own property- Now when
getPrice()is called, it looks forpriceonvueCourse, doesn't find it (we deleted the own property), and walks up the prototype chain to findpriceonreactCourse
The delete operator only affects own properties, not inherited ones. If you want to completely remove access to the inherited price, you'd need to delete it from reactCourse itself (which would affect all objects inheriting from it).
How do reference assignments work with objects?
let a = { value: 1 };
let b = a;
a.value = 2;
console.log(b.value);
a = { value: 3 };
console.log(b.value);The output is 2 then 2.
Objects are assigned by reference. b and a initially point to the same object, so changing a.value affects b. But a = { value: 3 } creates a new object and points a to it, leaving b pointing to the original object (which still has value: 2).
Quick Reference
| Concept | What It Tests | Key Insight |
|---|---|---|
var x = y = 5 | Implicit globals | Right-to-left assignment creates undeclared global |
Closure loop with var | Closures & scope | var is function-scoped; closures capture references |
| Detached method call | this binding | this is determined by call site, not definition |
| Function hoisting | Execution order | Function declarations shadow outer variables |
0.1 + 0.2 | Number representation | IEEE 754 can't represent all decimals exactly |
setTimeout(..., 0) | Event loop | Task queue runs after call stack clears |
'6' + 9 vs '6' - 9 | Type coercion | + concatenates with strings; - converts |
typeof null | Historical quirks | Unfixed bug from JavaScript's first version |
| Sparse arrays | Array internals | Empty slots are different from undefined values |
delete with prototypes | Prototype chain | Only removes own properties, not inherited |
1 < 2 < 3 | Operator evaluation | No chaining; left-to-right with coercion |
IIFE this context | Closures & this | IIFE creates new execution context |
Related Articles
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- JavaScript Closures Interview Guide - Understanding closures is essential for hooks and callbacks
- JavaScript Event Loop Interview Guide - How async JavaScript really works under the hood
- 7 Tricky TypeScript Interview Questions - Advanced type system questions that test deep knowledge
- Top 5 JavaScript Interview Mistakes - The most common interview failures and how to avoid them
