JavaScript Trick Questions — Code Output
🎯 JavaScript Trick Questions — Guess the Output
Section titled “🎯 JavaScript Trick Questions — Guess the Output”These are classic JavaScript gotcha questions that test your understanding of type coercion, equality, references, hoisting, closures, promises, and the event loop. Try to guess the output before reading the explanation!
1. Loose vs Strict Equality
Section titled “1. Loose vs Strict Equality”Q1: == vs ===
Section titled “Q1: == vs ===”console.log(0 == "0");console.log(0 === "0");console.log(null == undefined);console.log(null === undefined);👉 Click to reveal answer
Output:
truefalsetruefalseWhy?
==performs type coercion —"0"becomes0, so0 == 0istrue===checks both value and type —number≠string, sofalsenull == undefinedistrue(special JS rule for==)null === undefinedisfalse(different types)
Q2: Empty Array Comparisons
Section titled “Q2: Empty Array Comparisons”console.log([] == []);console.log([] === []);console.log([] == ![]);console.log([] == 0);👉 Click to reveal answer
Output:
falsefalsetruetrueWhy?
[] == []— references are different (two separate objects in memory)[] === []— same reason, strict equality checks reference identity[] == ![]—![]isfalse. Then[] == false→[]coerces to"", then"" == false→ both coerce to0, so0 == 0→true! 🤯[] == 0—[]→""→0, so0 == 0→true
Q3: Weird Number Coercions
Section titled “Q3: Weird Number Coercions”console.log([] + []);console.log([] + {});console.log({} + []);console.log({} + {});👉 Click to reveal answer
Output:
"""[object Object]""[object Object]""[object Object][object Object]"Why?
+with objects triggers string coercion via.toString()[].toString()→""→"" + ""→""[].toString()→"",{}.toString()→"[object Object]"→"" + "[object Object]"→"[object Object]"{} + []— behavior depends on context: in browser or with parens({} + [])→"[object Object]". In Node.js console,{} + []is parsed as an empty block + unary+[]→ returns0!{}.toString()+{}.toString()="[object Object][object Object]"
2. typeof Quirks
Section titled “2. typeof Quirks”Q4: typeof Edge Cases
Section titled “Q4: typeof Edge Cases”console.log(typeof null);console.log(typeof undefined);console.log(typeof []);console.log(typeof NaN);console.log(typeof typeof 1);👉 Click to reveal answer
Output:
"object""undefined""object""number""string"Why?
typeof null→"object"— historic bug in JS (cannot be fixed, would break existing code)typeof NaN→"number"— NaN is still a numeric typetypeof typeof 1→typeof "number"→"string"(typeof always returns a string)
Q5: typeof Function
Section titled “Q5: typeof Function”console.log(typeof function(){});console.log(typeof class {});console.log(typeof (() => {}));console.log(typeof async function() {});👉 Click to reveal answer
Output:
"function""function""function""function"Why?
- All function types return
"function"from typeof - Classes in JS are syntactical sugar over functions, so
typeof class {}is"function" - Async functions are still functions
3. NaN Is Not Equal to Itself
Section titled “3. NaN Is Not Equal to Itself”Q6: NaN Comparisons
Section titled “Q6: NaN Comparisons”console.log(NaN === NaN);console.log(NaN == NaN);console.log(Object.is(NaN, NaN));console.log([NaN].indexOf(NaN));console.log([NaN].includes(NaN));👉 Click to reveal answer
Output:
falsefalsetrue-1trueWhy?
NaN === NaN→false— NaN is the only JS value not equal to itself (IEEE 754 standard)Object.is(NaN, NaN)→true—Object.istreats NaN as equal (ES6 fix)[NaN].indexOf(NaN)→-1— indexOf uses strict equality, so can’t find NaN[NaN].includes(NaN)→true— includes usesSameValueZeroalgorithm (ES6 fix)
4. Truthy vs Falsy
Section titled “4. Truthy vs Falsy”Q7: Falsy Values
Section titled “Q7: Falsy Values”console.log(Boolean(false));console.log(Boolean(0));console.log(Boolean(""));console.log(Boolean(null));console.log(Boolean(undefined));console.log(Boolean(NaN));console.log(Boolean(-0));console.log(Boolean(0n));👉 Click to reveal answer
Output:
falsefalsefalsefalsefalsefalsefalsefalseThere are exactly 8 falsy values in JavaScript:
false, 0, -0, 0n, "", '', ``, null, undefined, NaNEverything else is truthy, including [], {}, "false", "0", -1, Infinity.
Q8: Truthy Gotchas
Section titled “Q8: Truthy Gotchas”if ([]) console.log("[] is truthy");if ({}) console.log("{} is truthy");if ("false") console.log("'false' is truthy");if ("0") console.log("'0' is truthy");if (-1) console.log("-1 is truthy");
console.log([] ? "true" : "false");console.log("" ? "true" : "false");👉 Click to reveal answer
Output:
[] is truthy{} is truthy'false' is truthy'0' is truthy-1 is truthytruefalseWhy?
- Empty arrays and objects are truthy — they are objects, not primitives
"false"is a non-empty string — truthy!"0"is also a non-empty string — truthy! Even though"0" == 0is true,Boolean("0")is true-1is non-zero → truthy
5. Addition and Coercion
Section titled “5. Addition and Coercion”Q9: String vs Number Addition
Section titled “Q9: String vs Number Addition”console.log(1 + "2");console.log("1" + 2);console.log(1 + 2 + "3");console.log("1" + 2 + 3);console.log(1 - "2");console.log("5" * "3");console.log("10" / "2");👉 Click to reveal answer
Output:
"12""12""33""123"-1155Why?
+with a string triggers string concatenation1 + "2"→"12"(number coerced to string)1 + 2 + "3"→3 + "3"→"33"(left-associative: first add numbers, then concat)"1" + 2 + 3→"12" + 3→"123"(first concat, keeps going)-,*,/always coerce to numbers —1 - "2"→1 - 2→-1
Q10: Unary Plus and Minus
Section titled “Q10: Unary Plus and Minus”console.log(+"123");console.log(+true);console.log(+false);console.log(+null);console.log(+undefined);console.log(+"");👉 Click to reveal answer
Output:
123100NaN0Why?
- Unary
+coerces to number +true→1,+false→0,+null→0+undefined→NaN(undefined is not a valid number)+""→0(empty string → 0)
6. Reference vs Value
Section titled “6. Reference vs Value”Q11: Object vs Primitive Assignment
Section titled “Q11: Object vs Primitive Assignment”let a = 10;let b = a;b = 20;console.log(a);
let x = { value: 10 };let y = x;y.value = 20;console.log(x.value);
let z = x;z = { value: 30 };console.log(x.value);👉 Click to reveal answer
Output:
102020Why?
- Primitives are assigned by value —
aandbare independent copies - Objects are assigned by reference —
xandypoint to the same object y.value = 20modifies the shared object →x.valueis also20z = { value: 30 }creates a new object —znow points somewhere else,xis unchanged
Q12: Array References
Section titled “Q12: Array References”const arr1 = [1, 2, 3];const arr2 = arr1;arr2.push(4);console.log(arr1);
const arr3 = [1, 2, 3];const arr4 = arr3;arr4 = [4, 5, 6]; // What happens here?👉 Click to reveal answer
Output:
[1, 2, 3, 4]TypeError: Assignment to constant variableWhy?
arr1andarr2point to the same array — mutatingarr2also affectsarr1arr4 = [4, 5, 6]— ❌constprevents reassignment (but allows mutation)
7. Hoisting
Section titled “7. Hoisting”Q13: var vs let Hoisting
Section titled “Q13: var vs let Hoisting”console.log(a);var a = 5;
console.log(b);let b = 5;👉 Click to reveal answer
Output:
undefinedReferenceError: Cannot access 'b' before initializationWhy?
varis hoisted and initialized withundefined— accessible before declarationletandconstare hoisted but not initialized — they’re in the Temporal Dead Zone (TDZ)
Q14: Function Hoisting
Section titled “Q14: Function Hoisting”console.log(foo());function foo() { return "function declaration"; }
console.log(bar());var bar = function() { return "function expression"; };
console.log(baz());let baz = function() { return "arrow function"; };👉 Click to reveal answer
Output:
"function declaration"TypeError: bar is not a functionReferenceError: Cannot access 'baz' before initializationWhy?
functiondeclarations are fully hoisted — can be called before definitionvar bar = function()—baris hoisted asundefined, but callingundefined()throwsTypeErrorlet baz = ...— TDZ, can’t access before initialization
Q15: Var in Block Scope
Section titled “Q15: Var in Block Scope”if (true) { var x = 10; let y = 20;}console.log(x);console.log(y);👉 Click to reveal answer
Output:
10ReferenceError: y is not definedWhy?
varis function-scoped, not block-scoped — it leaks out of{}blocksletis block-scoped — stays inside the{}block
8. Closure Gotchas
Section titled “8. Closure Gotchas”Q16: Classic Closure Loop Problem
Section titled “Q16: Classic Closure Loop Problem”for (var i = 1; i <= 3; i++) { setTimeout(() => console.log(i), 100);}
for (let j = 1; j <= 3; j++) { setTimeout(() => console.log(j), 100);}👉 Click to reveal answer
Output:
444123Why?
var: All three closures share the sameivariable. By the time the callbacks run (100ms later), the loop has finished andiis4.let:letcreates a new binding for each iteration. Each closure captures its ownjvalue.
Q17: Closure Independence
Section titled “Q17: Closure Independence”function outer() { let count = 0; return function inner() { count++; return count; };}
const fn1 = outer();const fn2 = outer();console.log(fn1());console.log(fn1());console.log(fn2());👉 Click to reveal answer
Output:
121Why?
- Each call to
outer()creates a newcountvariable in a new closure scope fn1andfn2have independent count variables
Q18: IIFE Closure Fix
Section titled “Q18: IIFE Closure Fix”for (var i = 0; i < 3; i++) { setTimeout((function(x) { return function() { console.log(x); }; })(i), 100);}👉 Click to reveal answer
Output:
012Why?
- The IIFE creates a new scope each iteration, capturing
iasx - Each callback has its own
xvalue (0, 1, 2)
9. this Binding
Section titled “9. this Binding”Q19: this in Different Contexts
Section titled “Q19: this in Different Contexts”const obj = { name: "Alice", regular: function() { console.log(this.name); }, arrow: () => console.log(this.name)};
obj.regular();obj.arrow();
const regularFn = obj.regular;regularFn();👉 Click to reveal answer
Output:
"Alice"undefinedundefinedWhy?
obj.regular()— Implicit binding:this=obj(object before the dot)obj.arrow()— Arrow functions don’t have their ownthis. They inheritthisfrom the enclosing scope (here: global/undefined in strict)regularFn()— Default binding:this=window/undefined(no object context)
Q20: this Inside setTimeout
Section titled “Q20: this Inside setTimeout”const obj = { name: "Alice", sayName: function() { setTimeout(function() { console.log(this.name); }, 100); setTimeout(() => console.log(this.name), 100); }};
obj.sayName();👉 Click to reveal answer
Output:
undefined (or "" in browser)"Alice"Why?
- Regular function in setTimeout:
thisis the global object — NOTobj - Arrow function in setTimeout: Arrow functions inherit
thisfromsayName’s context, which isobj
Q21: Binding Precedence
Section titled “Q21: Binding Precedence”function greet() { console.log(this.name); }
const user = { name: "Alice" };const admin = { name: "Admin" };
const boundGreet = greet.bind(user);boundGreet.call(admin);👉 Click to reveal answer
Output:
"Alice"Why?
bindhas the highest precedence (afternew)- Once a function is bound with
.bind(), subsequent.call()or.apply()cannot override the binding - Exception:
newoperator overridesbind
10. Array Quirks
Section titled “10. Array Quirks”Q22: Array Length
Section titled “Q22: Array Length”const arr = [1, 2, 3];arr.length = 0;console.log(arr);
arr.length = 5;console.log(arr);
arr[10] = 10;console.log(arr.length);👉 Click to reveal answer
Output:
[][empty × 5]11Why?
- Setting
arr.length = 0empties the array - Setting
arr.length = 5creates empty slots (sparse array) - Setting
arr[10] = 10makes length = 11 (index + 1)
Q23: Array delete and splice
Section titled “Q23: Array delete and splice”const arr = [1, 2, 3, 4];delete arr[1];console.log(arr);console.log(arr.length);
const arr2 = [1, 2, 3, 4];arr2.splice(1, 1);console.log(arr2);console.log(arr2.length);👉 Click to reveal answer
Output:
[1, empty, 3, 4]4[1, 3, 4]3Why?
delete arr[1]removes the value but keeps the slot — length stays 4arr2.splice(1, 1)removes the element and shifts everything — length becomes 3
11. Object Quirks
Section titled “11. Object Quirks”Q24: Object Keys
Section titled “Q24: Object Keys”const obj = { 1: "one", "1": "two"};console.log(obj);👉 Click to reveal answer
Output:
{"1": "two"}Why?
- Object keys are always strings (or Symbols)
- Numeric keys
1and string keys"1"are the same key - The second assignment overwrites the first
Q25: Object Key Order
Section titled “Q25: Object Key Order”const obj = { b: 1, a: 2, 3: 3, 1: 4, [Symbol()]: 5};
console.log(Object.keys(obj));console.log(Object.values(obj));console.log(JSON.stringify(obj));👉 Click to reveal answer
Output:
["1", "3", "b", "a"][4, 3, 1, 2]{"1":4,"3":3,"b":1,"a":2}Why?
- Object property order: Integer indices → String keys (insertion) → Symbols
- Symbols are not shown in JSON/keys output
Q26: Property Access
Section titled “Q26: Property Access”const obj = { name: "Alice" };console.log(obj.name);console.log(obj["name"]);// console.log(obj[name]); // ReferenceError: name is not definedconsole.log(obj.nickname?.length);👉 Click to reveal answer
Output:
"Alice""Alice"undefinedWhy?
obj.nameandobj["name"]are equivalentobj[name]treatsnameas a variable — would throw ifnameis not definedobj.nickname?.length— optional chaining: returnsundefinedwithout throwing
12. Coercion Tricks
Section titled “12. Coercion Tricks”Q27: Coercion in Conditionals
Section titled “Q27: Coercion in Conditionals”if ("0") console.log("1");if ("") console.log("2");if ([]) console.log("3");if ([0]) console.log("4");if ([1, 2]) console.log("5");👉 Click to reveal answer
Output:
1345Why?
"0"is a non-empty string → truthy""is empty → falsy[]is an object → truthy[0]is also an object → truthy[1, 2]is also an object → truthy
Q28: Bizarre Coercion Chain
Section titled “Q28: Bizarre Coercion Chain”console.log(true + false);console.log(true + true);console.log(12 / "6");console.log("number" + 15 + 3);console.log(15 + 3 + "number");console.log([1] > null);console.log("foo" + +"bar");👉 Click to reveal answer
Output:
122"number153""18number"true"fooNaN"Why?
true + false→1 + 0→115 + 3 + "number"→18 + "number"→"18number"[1] > null→"1" > 0→1 > 0→true+"bar"→NaN, so"foo" + NaN→"fooNaN"
Q29: Plus Operator Precedence
Section titled “Q29: Plus Operator Precedence”console.log(1 + 2 + 3 + "4");console.log("1" + 2 + 3 + 4);console.log(1 + "2" + 3 + 4);console.log(1 + 2 + "3" + 4);👉 Click to reveal answer
Output:
"64""1234""1234""334"Why?
1 + 2 + 3 + "4"→6 + "4"→"64"(adds numbers first, then concat)"1" + 2 + 3 + 4→"1234"(once string, always string)1 + 2 + "3" + 4→3 + "3" + 4→"33" + 4→"334"
13. parseInt Quirks
Section titled “13. parseInt Quirks”Q30: parseInt Edge Cases
Section titled “Q30: parseInt Edge Cases”console.log(parseInt(""));console.log(parseInt("0xF"));console.log(parseInt("0xf"));console.log(parseInt("08"));console.log(parseInt("123abc"));console.log(parseInt("abc123"));console.log(parseInt(" 123 "));console.log(parseInt(0.0000005));console.log(parseInt(0.5));👉 Click to reveal answer
Output:
NaN15158123NaN12350Why?
parseInt("")→NaNparseInt("0xF")→15(hex)parseInt("123abc")→123(parses up to first non-digit)parseInt(0.0000005)→5(String is"5e-7", reads"5")parseInt(0.5)→0(String is"0.5", stops at.)
Q31: parseInt With Radix
Section titled “Q31: parseInt With Radix”["1", "2", "3"].map(parseInt);👉 Click to reveal answer
Output:
[1, NaN, NaN]Why?
.map(parseInt)is equivalent to.map((item, index) => parseInt(item, index))parseInt("1", 0)→1(radix 0 = auto)parseInt("2", 1)→NaN(radix must be 2-36)parseInt("3", 2)→NaN(binary has no digit3)
14. Operator Precedence
Section titled “14. Operator Precedence”Q32: Comma Operator
Section titled “Q32: Comma Operator”console.log((1, 2, 3));console.log(1, 2, 3);👉 Click to reveal answer
Output:
31 2 3Why?
- The comma operator returns the last value:
(1, 2, 3)→3 console.log(1, 2, 3)— comma separates arguments, so it logs all three
Q33: Assignment vs Comparison
Section titled “Q33: Assignment vs Comparison”let x = 0;if (x = 10) { console.log("true");}console.log(x);👉 Click to reveal answer
Output:
"true"10Why?
x = 10is an assignment, not a comparison — returns1010is truthy, so the if-block executes- Common bug: accidentally using
=instead of===
Q34: Chained Comparisons
Section titled “Q34: Chained Comparisons”console.log(3 > 2 > 1);console.log(1 < 2 < 3);👉 Click to reveal answer
Output:
falsetrueWhy?
3 > 2 > 1→true > 1→1 > 1→false1 < 2 < 3→true < 3→1 < 3→true- Correct way:
3 > 2 && 2 > 1
15. Prototypes & Inheritance
Section titled “15. Prototypes & Inheritance”Q35: Instanceof Check
Section titled “Q35: Instanceof Check”console.log([] instanceof Array);console.log([] instanceof Object);console.log({} instanceof Object);console.log({} instanceof Array);console.log(null instanceof Object);console.log(undefined instanceof Object);👉 Click to reveal answer
Output:
truetruetruefalsefalsefalseWhy?
instanceofchecks the prototype chain[]is an instance ofArrayand alsoObject(Array inherits from Object)nullandundefinedare primitives →instanceofreturnsfalse
Q36: Constructor Without new
Section titled “Q36: Constructor Without new”function Person(name) { this.name = name; return { name: "overridden" };}
const p1 = new Person("Alice");const p2 = Person("Bob");
console.log(p1);console.log(p2);// console.log(name); // "Bob" in non-strict mode (global leak)👉 Click to reveal answer
Output (non-strict):
{ name: "overridden" }{ name: "overridden" }Why?
- With
new: If the constructor explicitly returns an object, that object is used instead ofthis - Without
new:thisis the global object —this.name = "Bob"leaks as a global - Both return the same explicit return object
{ name: "overridden" }
16. Destructuring & Spread
Section titled “16. Destructuring & Spread”Q37: Default Values in Destructuring
Section titled “Q37: Default Values in Destructuring”const { a = 10, b = 20 } = { a: undefined };console.log(a, b);
const [x = 1, y = 2, z = 3] = [0, null, undefined];console.log(x, y, z);👉 Click to reveal answer
Output:
10 200 null 3Why?
- Default values only apply when the value is
undefined - For objects:
aisundefined→ default10applies.bis missing → default20 - For arrays:
x=0(not undefined),y=null(not undefined),z=undefined→ default3 nullis notundefined, so the default does NOT apply toy
Q38: Spread Operator — Shallow Copy
Section titled “Q38: Spread Operator — Shallow Copy”const original = { a: 1, b: { c: 2 } };const copy = { ...original };copy.a = 99;copy.b.c = 99;
console.log(original.a);console.log(original.b.c);👉 Click to reveal answer
Output:
199Why?
- Spread
{...obj}creates a shallow copy copy.a = 99—ais a primitive, sooriginal.ais unaffectedcopy.b.c = 99—bis an object reference, so modifyingcopy.balso modifiesoriginal.b- For deep copy, use
structuredClone()orJSON.parse(JSON.stringify(obj))
Q39: Rest Operator in Destructuring
Section titled “Q39: Rest Operator in Destructuring”const [first, ...rest] = [1, 2, 3, 4, 5];const { x, ...others } = { x: 10, y: 20, z: 30 };
console.log(first, rest);console.log(x, others);👉 Click to reveal answer
Output:
1 [2, 3, 4, 5]10 { y: 20, z: 30 }Why?
- Rest operator
...collects remaining elements into a new array/object - In arrays:
restis[2, 3, 4, 5] - In objects:
othersis{ y: 20, z: 30 } - Rest must be the last element in destructuring
17. Promises & Async Edge Cases
Section titled “17. Promises & Async Edge Cases”Q40: Promise Chaining — Error Propagation
Section titled “Q40: Promise Chaining — Error Propagation”Promise.resolve(1) .then(x => x + 1) .then(x => { throw new Error(String(x)); }) .then(x => console.log("A:", x)) .catch(e => console.log("B:", e.message)) .then(x => console.log("C:", x));👉 Click to reveal answer
Output:
B: 2C: undefinedWhy?
1 → 2(first then),2 → throw Error("2")(second then)- Error is caught by
.catch()→ logs"B: 2" .catch()returnsundefined(no explicit return)- Next
.then()receivesundefined→ logs"C: undefined" - Important: A
.catch()after a throw recovers the chain — subsequent.then()still runs!
Q41: Promise.all — Short Circuit
Section titled “Q41: Promise.all — Short Circuit”const p1 = Promise.resolve("one");const p2 = Promise.reject("fail");const p3 = new Promise(resolve => setTimeout(() => resolve("three"), 1000));
Promise.all([p1, p2, p3]) .then(results => console.log("All:", results)) .catch(err => console.log("Error:", err));👉 Click to reveal answer
Output:
Error: failWhy?
Promise.all()short-circuits on the first rejectionp2rejects immediately with"fail"— no waiting forp3.catch()receives"fail"immediatelyp3is still pending but its result is ignored- For waiting on all (even failures), use
Promise.allSettled()
Q42: Async Error Handling
Section titled “Q42: Async Error Handling”async function test() { try { return await Promise.reject("FAIL"); } finally { console.log("finally"); }}
test().then(r => console.log("then:", r)).catch(e => console.log("catch:", e));👉 Click to reveal answer
Output:
finallycatch: FAILWhy?
await Promise.reject("FAIL")throws before thereturnfinallyblock always runs, even when an error is thrown- The error is not caught by
try(nocatchblock), so it propagates .catch()on the promise chain catches"FAIL"- Key:
finallydoes NOT swallow the error — it continues propagating
Q43: Promise.race Behavior
Section titled “Q43: Promise.race Behavior”const slow = new Promise(resolve => setTimeout(() => resolve("slow"), 500));const fast = new Promise((_, reject) => setTimeout(() => reject("fast error"), 100));
Promise.race([slow, fast]) .then(r => console.log("Winner:", r)) .catch(e => console.log("Error:", e));👉 Click to reveal answer
Output:
Error: fast errorWhy?
Promise.race()settles with the first promise to settle (resolve or reject)fastrejects at 100ms,slowresolves at 500ms- The race is won by
fast’s rejection —.catch()is called - Even though
slowresolves later, its result is ignored
18. Nullish Coalescing & Optional Chaining
Section titled “18. Nullish Coalescing & Optional Chaining”Q44: ?? vs ||
Section titled “Q44: ?? vs ||”console.log(0 || "default");console.log(0 ?? "default");console.log("" || "fallback");console.log("" ?? "fallback");console.log(null ?? "nullish");console.log(undefined ?? "nullish");console.log(false ?? "truthy");console.log(false || "truthy");👉 Click to reveal answer
Output:
"default"0"fallback""""nullish""nullish"false"truthy"Why?
||returns the first truthy value —0,"",false,null,undefinedall trigger the default??(nullish coalescing) returns the default only fornullorundefined0 ?? "default"→0(0 is not null/undefined)"" ?? "fallback"→""(empty string is not null/undefined)false ?? "truthy"→false- Use
??when0,"", orfalseare valid values!
Q45: Optional Chaining — Method Call
Section titled “Q45: Optional Chaining — Method Call”const obj1 = { greet: () => "hello" };const obj2 = {};
console.log(obj1.greet?.());console.log(obj2.greet?.());console.log(obj2.greet?.());👉 Click to reveal answer
Output:
"hello"undefinedundefinedWhy?
obj1.greet?.()—greetexists and is a function → calls it → returns"hello"obj2.greet?.()—greetisundefined→ optional chaining stops → returnsundefined- Without
?.,obj2.greet()would throwTypeError: obj2.greet is not a function
Q46: Nullish Assignment ??=
Section titled “Q46: Nullish Assignment ??=”let a = null;let b = undefined;let c = 0;let d = "hello";
a ??= "default A";b ??= "default B";c ??= "default C";d ??= "default D";
console.log(a, b, c, d);👉 Click to reveal answer
Output:
"default A" "default B" 0 "hello"Why?
??=(logical nullish assignment) assigns the value only if the variable isnullorundefinedaisnull→ assigned"default A"bisundefined→ assigned"default B"cis0(not null/undefined) → NOT assigneddis"hello"(not null/undefined) → NOT assigned
19. Set, Map & Collections
Section titled “19. Set, Map & Collections”Q47: Set — Reference Equality
Section titled “Q47: Set — Reference Equality”const set = new Set();set.add([1, 2]);set.add([1, 2]);set.add({ a: 1 });set.add({ a: 1 });console.log(set.size);
const obj = { x: 10 };set.add(obj);set.add(obj);console.log(set.size);👉 Click to reveal answer
Output:
45Why?
Setuses SameValueZero equality — which is reference equality for objects[1, 2]and[1, 2]are different array instances → both added (count: 2){ a: 1 }and{ a: 1 }are different object instances → both added (count: 4)objis added once, the secondadd(obj)does nothing (same reference!) → count: 5
Q48: Map — Object Keys
Section titled “Q48: Map — Object Keys”const map = new Map();map.set("1", "string key");map.set(1, "number key");map.set(true, "boolean key");
console.log(map.get("1"));console.log(map.get(1));console.log(map.get(true));console.log(map.size);
// vs Objectconst obj = {};obj["1"] = "string";obj[1] = "number"; // overwrites "1"!console.log(obj);👉 Click to reveal answer
Output:
"string key""number key""boolean key"3{"1": "number"}Why?
Maptreats keys by their actual type —"1"(string) and1(number) are different keys- Object coerces all keys to strings —
obj[1]overwritesobj["1"] Mappreserves all 3 distinct keys → size is3Mapalso accepts objects, functions, and evenNaNas keys!
Q49: WeakMap — No Iteration
Section titled “Q49: WeakMap — No Iteration”const wm = new WeakMap();let obj = { name: "temporary" };wm.set(obj, "some value");
console.log(wm.get(obj));// console.log(wm.size); // undefined// for (let [k, v] of wm) {} // TypeError
obj = null; // obj becomes eligible for garbage collection// The key-value pair is automatically removed from the WeakMap👉 Click to reveal answer
Output:
"some value"Why?
WeakMapkeys must be objects, and the reference is weak (doesn’t prevent GC)WeakMaphas nosizeproperty and no iteration methods (noforEach, nofor...of)- When
obj = null— the original object is garbage collected, and the WeakMap entry is automatically removed - Use case: storing private data attached to DOM elements without memory leaks
WeakSetworks the same way (object keys only, no iteration)
20. Generators & Iterators
Section titled “20. Generators & Iterators”Q50: Generator Basics
Section titled “Q50: Generator Basics”function* countUp() { yield 1; yield 2; yield 3;}
const gen = countUp();console.log(gen.next());console.log(gen.next());console.log(gen.next());console.log(gen.next());👉 Click to reveal answer
Output:
{ value: 1, done: false }{ value: 2, done: false }{ value: 3, done: false }{ value: undefined, done: true }Why?
- Generators return an iterator via
next() - Each
next()returns{ value, done }— resumes execution until the nextyield - When all
yields are exhausted, returns{ value: undefined, done: true } yieldcan also receive values:const x = yield;
Q51: Generator Delegation
Section titled “Q51: Generator Delegation”function* gen1() { yield 1; yield 2;}
function* gen2() { yield "a"; yield* gen1(); yield "b";}
console.log([...gen2()]);👉 Click to reveal answer
Output:
["a", 1, 2, "b"]Why?
yield*delegates to another generator (or any iterable)- All values from
gen1()are yielded one by one intogen2() [...gen2()]spreads all yielded values into an array- Works with any iterable:
yield* [1, 2, 3]works too!
Q52: Generator Return and Throw
Section titled “Q52: Generator Return and Throw”function* numbers() { try { yield 1; yield 2; yield 3; } catch (e) { console.log("Caught:", e); }}
const gen = numbers();console.log(gen.next());console.log(gen.return("early exit"));console.log(gen.next());👉 Click to reveal answer
Output:
{ value: 1, done: false }{ value: "early exit", done: true }{ value: undefined, done: true }Why?
gen.return(value)terminates the generator early with the given valuegen.throw(error)throws an error at the lastyieldpoint (caught inside if there’s a try/catch)- After
returnorthrow, the generator is done — subsequentnext()returns{ done: true }
21. Classes & Inheritance
Section titled “21. Classes & Inheritance”Q53: Super in Derived Classes
Section titled “Q53: Super in Derived Classes”class Parent { constructor() { this.name = "Parent"; }}
class Child extends Parent { constructor() { // this.name = "Child"; // What if we don't call super? super(); this.name = "Child"; }}
// What happens if we comment out super()?console.log(new Child().name);👉 Click to reveal answer
Output:
"Child"But if super() is commented out:
ReferenceError: Must call super constructor in derived class before accessing 'this'Why?
- In a derived class constructor,
super()must be called before usingthis super()calls the parent constructor to initializethis- After
super(),this.name = "Parent", thenthis.name = "Child"overwrites it - Without
super(),thisis uninitialized → throwsReferenceError
Q54: Static Methods — Inheritance
Section titled “Q54: Static Methods — Inheritance”class Parent { static greet() { return "Hello from Parent"; } static getMessage() { return this.greet(); }}
class Child extends Parent { static greet() { return "Hello from Child"; }}
console.log(Parent.getMessage());console.log(Child.getMessage());👉 Click to reveal answer
Output:
"Hello from Parent""Hello from Child"Why?
- Static methods are inherited by subclasses
- Inside a static method,
thisrefers to the class itself (not an instance) Parent.getMessage()—thisisParent→ callsParent.greet()Child.getMessage()—thisisChild→ callsChild.greet()(polymorphism!)- Static methods support polymorphism just like instance methods
Q55: Private Fields
Section titled “Q55: Private Fields”class Person { #secret = "hidden"; getSecret() { return this.#secret; }}
const p = new Person();console.log(p.getSecret());console.log(p.#secret);👉 Click to reveal answer
Output:
"hidden"SyntaxError: Private field '#secret' must be declared in an enclosing classWhy?
#makes a field truly private (enforced at the engine level)- Can only be accessed inside the class
- Accessing
p.#secretfrom outside throws aSyntaxError(not a runtime error) - Private fields are not just a naming convention (unlike
_private) — they are truly inaccessible - No way to access them with
Object.keys(),Reflect.ownKeys(), or any other trick
22. Timers & Execution Order
Section titled “22. Timers & Execution Order”Q56: setTimeout Inside Promises
Section titled “Q56: setTimeout Inside Promises”console.log(1);
setTimeout(() => { console.log(2); Promise.resolve().then(() => console.log(3));}, 0);
Promise.resolve().then(() => { console.log(4); setTimeout(() => console.log(5), 0);});
console.log(6);👉 Click to reveal answer
Output:
164235Why?
- Synchronous:
1,6 - Microtask queue: Promise
.then—console.log(4), thensetTimeout(() => console.log(5))is scheduled - Macrotask 1: First
setTimeout—console.log(2)runs, then microtaskconsole.log(3)is queued - Microtask:
console.log(3)runs - Macrotask 2: Second
setTimeout—console.log(5)runs
Order: sync > microtasks > macro1 > micro(inside macro1) > macro2
Q57: Multiple setTimeout Order
Section titled “Q57: Multiple setTimeout Order”setTimeout(() => console.log("A"), 100);setTimeout(() => console.log("B"), 0);setTimeout(() => console.log("C"), 50);setTimeout(() => console.log("D"), 0);console.log("E");👉 Click to reveal answer
Output:
EBDCAWhy?
console.log("E")runs first (synchronous)setTimeout(fn, 0)callbacks are queued in order:B, thenDsetTimeout(fn, 50)→Cis queued at 50mssetTimeout(fn, 100)→Ais queued at 100ms- Even though timers are delayed at least the specified time, they execute in order of the timer queue
- All three timers (0, 50, 100) don’t expire simultaneously — they execute when their time is up
Q58: setInterval Behavior
Section titled “Q58: setInterval Behavior”let count = 0;const interval = setInterval(() => { console.log(++count); if (count === 3) clearInterval(interval);}, 100);👉 Click to reveal answer
Output:
123Why?
setInterval(fn, 100)callsfnevery 100ms- Each call increments and logs
count - When
count === 3,clearInterval(interval)stops the interval - Note: If the callback takes longer than the interval, calls can stack up
- Best practice: Use recursive
setTimeoutinstead for guaranteed gap between calls
23. JSON, toPrimitive & Coercion
Section titled “23. JSON, toPrimitive & Coercion”Q59: JSON.stringify Edge Cases
Section titled “Q59: JSON.stringify Edge Cases”console.log(JSON.stringify(undefined));console.log(JSON.stringify(function() {}));console.log(JSON.stringify(Symbol()));console.log(JSON.stringify([undefined, function() {}, Symbol()]));console.log(JSON.stringify({ a: undefined, b: function() {}, c: Symbol(), d: 42 }));console.log(JSON.stringify({ a: NaN, b: Infinity, c: -Infinity }));👉 Click to reveal answer
Output:
undefinedundefinedundefined[null, null, null]{"d":42}{"a":null,"b":null,"c":null}Why?
JSON.stringifyomitsundefined, functions, and Symbols in objects- In arrays, they become
null(to preserve array indices/sparseness) NaN,Infinity,-Infinityare all converted tonullJSON.stringify(undefined)returnsundefined(not the string"undefined")- Circular references throw
TypeError
Q60: Symbol.toPrimitive
Section titled “Q60: Symbol.toPrimitive”const obj = { value: 42, [Symbol.toPrimitive](hint) { console.log("hint:", hint); return hint === "string" ? `Value is ${this.value}` : this.value; }};
console.log(String(obj));console.log(+obj);console.log(`${obj}`);👉 Click to reveal answer
Output:
hint: stringValue is 42hint: number42hint: stringValue is 42Why?
Symbol.toPrimitiveoverrides the default coercion behaviorString(obj)— hint is"string"+obj— hint is"number"${obj}— hint is"string"(template literals)- Without this, JS would use
toString()andvalueOf()with specific ordering rules - This is the most powerful way to control object-to-primitive conversion
Q61: valueOf vs toString Priority
Section titled “Q61: valueOf vs toString Priority”const obj = { valueOf() { return 42; }, toString() { return "hello"; }};
console.log(obj + 0);console.log(String(obj));console.log(`${obj}`);👉 Click to reveal answer
Output:
42"hello""hello"Why?
obj + 0—+operator with a number hints atnumber.valueOf()returns42String(obj)— explicitly asks for string.toString()returns"hello"- Intersting: in some browsers with the older spec, template literals might use
valueOf()but modern spec usestoString()for string context - The priority:
Symbol.toPrimitive>valueOf>toStringfor numeric hints - For string hints:
Symbol.toPrimitive>toString>valueOf
24. Regex & Beyond
Section titled “24. Regex & Beyond”Q62: Regex — LastIndex Quirk
Section titled “Q62: Regex — LastIndex Quirk”const regex = /abc/g;const str = "abc abc abc";
console.log(regex.test(str));console.log(regex.test(str));console.log(regex.test(str));console.log(regex.test(str));👉 Click to reveal answer
Output:
truetruetruefalseWhy?
- The
gflag makes the regex stateful — it trackslastIndex - After each
test(),lastIndexadvances to the end of the match - Match 1:
lastIndex = 3, Match 2:lastIndex = 7, Match 3:lastIndex = 11 - Match 4: no more matches →
lastIndexresets to0, returnsfalse - Common bug: Using a global regex in a loop can skip alternate matches!
- Fix: Reset
lastIndex = 0manually, or use.match()non-global
Q63: Date — Month Indexing
Section titled “Q63: Date — Month Indexing”console.log(new Date(2024, 0, 1));console.log(new Date(2024, 11, 25));console.log(new Date(2024, 12, 1));👉 Click to reveal answer
Output:
2024-01-01T00:00:00.000Z2024-12-25T00:00:00.000Z2025-01-01T00:00:00.000ZWhy?
- JavaScript
Datemonths are 0-indexed: January =0, December =11 new Date(2024, 0, 1)→ January 1, 2024 ✅new Date(2024, 11, 25)→ December 25, 2024 ✅new Date(2024, 12, 1)→ Month12overflows to January 2025! (12 = month 0 of next year)- Always remember: Months are 0-11, not 1-12!
- Day of month (3rd parameter) is 1-indexed (which adds to the confusion)
Q64: parseInt vs Number
Section titled “Q64: parseInt vs Number”console.log(Number("123abc"));console.log(parseInt("123abc"));console.log(Number(""));console.log(parseInt(""));console.log(Number(null));console.log(parseInt(null));👉 Click to reveal answer
Output:
NaN1230NaN0NaNWhy?
Number("123abc")→NaN—Number()is strict, requires the entire string to be numericparseInt("123abc")→123—parseInt()parses left to right until it hits a non-digitNumber("")→0— empty string coerces to0(!)parseInt("")→NaN— no digits to parseNumber(null)→0— null coerces to0parseInt(null)→NaN—String(null)is"null", can’t parse that
25. Advanced Edge Cases
Section titled “25. Advanced Edge Cases”Q65: Function.length — Default Parameters
Section titled “Q65: Function.length — Default Parameters”function f1(a, b, c) {}function f2(a, b = 1, c) {}function f3(a, ...rest) {}
console.log(f1.length);console.log(f2.length);console.log(f3.length);👉 Click to reveal answer
Output:
311Why?
function.lengthcounts the number of parameters before the first default value or rest parameterf1(a, b, c)— all regular params → length is3f2(a, b = 1, c)—bhas a default → onlyacounts → length is1f3(a, ...rest)— rest parameter doesn’t count → onlya→ length is1- This can cause subtle bugs with libraries that check
fn.length!
Q66: Arrow Function — No arguments
Section titled “Q66: Arrow Function — No arguments”function regular() { console.log(arguments[0], arguments[1]);}
const arrow = () => { // console.log(arguments); // What happens here?};
regular(1, 2);
const arrowWithRest = (...args) => { console.log(args[0], args[1]);};arrowWithRest(3, 4);👉 Click to reveal answer
Output:
1 23 4Why?
- Regular functions have the
argumentsobject (array-like, all passed args) - Arrow functions do NOT have their own
arguments— accessing it would throwReferenceError - Fix: Use rest parameters
...argsinstead - Arrow functions also cannot:
- Be used with
new(no[[Construct]]) - Have a
prototypeproperty - Be generators (no
function*syntax)
- Be used with
Q67: Conditional Object Properties
Section titled “Q67: Conditional Object Properties”const flag = true;const obj = { a: 1, ...(flag && { b: 2 }), ...(!flag && { c: 3 }),};
console.log(obj);👉 Click to reveal answer
Output:
{ a: 1, b: 2 }Why?
flag && { b: 2 }→true && { b: 2 }→{ b: 2 }→ spread into object!flag && { c: 3 }→false && { c: 3 }→false→ spreadfalseinto object does nothing- This is a common pattern for conditional object properties
- If
flagwerefalse:{ a: 1 }(nob) - Works because spreading
false/null/undefinedis a no-op in objects
Q68: Array Sort Stability
Section titled “Q68: Array Sort Stability”const items = [ { name: "A", priority: 1 }, { name: "B", priority: 1 }, { name: "C", priority: 2 }, { name: "D", priority: 2 },];
items.sort((a, b) => a.priority - b.priority);console.log(items.map(i => i.name).join(""));👉 Click to reveal answer
Output (ES2019+):
"ABDC"Why?
- Since ES2019,
Array.sort()is stable — equal elements maintain their original order A(priority 1) andB(priority 1) keep original order:AthenBC(priority 2) andD(priority 2) keep original order:CthenD- Before ES2019, sort order for equal elements was implementation-dependent
- Stable sort is important for multi-key sorting (sort by city, then by name)
📊 Quick Reference — Must-Know Gotchas
Section titled “📊 Quick Reference — Must-Know Gotchas”| Expression | Result | Why |
|---|---|---|
[] == [] | false | Different references |
[] == ![] | true | Coercion chain: [] → "" → 0, ![] → false → 0 |
null == undefined | true | Special == rule |
null === undefined | false | Different types |
NaN === NaN | false | IEEE 754 spec |
typeof null | "object" | Historic bug |
typeof NaN | "number" | NaN is still numeric |
1 + "2" | "12" | String wins in + |
"2" - 1 | 1 | Number wins in -, *, / |
[] + [] | "" | Both toString to "" |
parseInt(0.0000005) | 5 | String is "5e-7" |
3 > 2 > 1 | false | Chained as true > 1 |
(1, 2, 3) | 3 | Comma operator returns last |
0 ?? "default" | 0 | ?? checks null/undefined only |
"" ?? "fallback" | "" | ?? checks null/undefined only |
undefined, fn, Symbol in obj | omitted | JSON.stringify skips them |
.map(parseInt) | [1, NaN, NaN] | Radix is the index! |
🧠 Key Takeaways
Section titled “🧠 Key Takeaways”- Always use
===—==with coercion causes unpredictable bugs - Objects are truthy — even empty arrays and objects
- NaN is the only value not equal to itself — use
Number.isNaN()to check typeof nullis"object"— historic bug, check withvalue === nullvaris not block-scoped — useletandconst- Arrow functions don’t have their own
this— they inherit it - Microtasks (Promise) run before macrotasks (setTimeout)
- Default
.sort()converts to strings — always pass a compare function - Closures capture variables, not values —
letfixes the loop problem parseIntsecond argument is the radix — be careful with.map(parseInt)!??is different from||—??only checks null/undefined- Spread is shallow — nested objects are still references
Maphas type-safe keys —"1"and1are different keys- Private fields
#are truly private — no way to access from outside - Promise
.catch()recovers the chain — subsequent.then()still runs
Back to: JavaScript Interview Overview