Chapter 13: The Interview Gauntlet - Common Tricks and Gotchas
Type Coercion Mysteries
// The infamous equality comparisons console.log([] == ![]); // true (wat?!) // Explanation: // [] == ![] // [] == false (! converts [] to false) // "" == false ([] converts to "") // 0 == false ("" converts to 0) // true (0 == 0) console.log([] + []); // "" (empty string) console.log({} + []); // 0 (in some contexts) console.log([] + {}); // "[object Object]" console.log({} + {}); // "[object Object][object Object]" // Truthy/falsy gotchas console.log(Boolean([])); // true (empty array is truthy!) console.log(Boolean({})); // true (empty object is truthy!) console.log(Boolean("")); // false console.log(Boolean("0")); // true (string "0" is truthy!) console.log(Boolean(0)); // false console.log(Boolean(NaN)); // false // typeof surprises console.log(typeof null); // "object" (historical bug) console.log(typeof undefined); // "undefined" console.log(typeof []); // "object" console.log(typeof function(){}); // "function" console.log(typeof NaN); // "number"
The this Binding Puzzle
// Global context function globalFunction() { console.log(this); // Window (browser) or global (Node.js) in non-strict mode } // Object method const obj = { name: "Alice", greet: function() { console.log(this.name); // "Alice" // Nested function loses context function inner() { console.log(this.name); // undefined (this is global/undefined) } inner(); // Arrow function preserves context const arrowInner = () => { console.log(this.name); // "Alice" }; arrowInner(); } }; // Call, apply, bind function introduce(greeting, punctuation) { return `${greeting}, I'm ${this.name}${punctuation}`; } const person = { name: "Bob" }; console.log(introduce.call(person, "Hello", "!")); // "Hello, I'm Bob!" console.log(introduce.apply(person, ["Hi", "."])); // "Hi, I'm Bob." const boundIntroduce = introduce.bind(person, "Hey"); console.log(boundIntroduce("?")); // "Hey, I'm Bob?"
Event Loop and Microtask Queue
console.log("1"); setTimeout(() => console.log("2"), 0); Promise.resolve().then(() => console.log("3")); Promise.resolve().then(() => { console.log("4"); setTimeout(() => console.log("5"), 0); }); setTimeout(() => console.log("6"), 0); console.log("7"); // Output: 1, 7, 3, 4, 2, 6, 5 // Explanation: // 1. Synchronous code runs first: 1, 7 // 2. Microtasks (Promises) run next: 3, 4 // 3. Macrotasks (setTimeout) run last: 2, 6, 5
Variable Hoisting Edge Cases
// Function vs variable hoisting precedence console.log(typeof foo); // "function" var foo = "I'm a variable"; function foo() { return "I'm a function"; } console.log(typeof foo); // "string" // Explanation: Function declarations are hoisted before variable declarations // But variable assignments happen in order // Temporal Dead Zone gotcha function tdz() { console.log(typeof x); // "undefined" - x doesn't exist yet if (true) { console.log(typeof x); // ReferenceError - x is in TDZ here let x = "I'm block scoped"; } }
Closure Memory Leaks
// Memory leak example function createLeak() { const hugeArray = new Array(1000000).fill("data"); return function() { // This closure keeps the entire hugeArray in memory // even though it doesn't use it return "I'm a small function"; }; } // Better approach function createNoLeak() { const hugeArray = new Array(1000000).fill("data"); const summary = hugeArray.length; // Extract what you need return function() { return `Array had ${summary} elements`; }; }
Classic Interview Questions
Question 1: What's the output?
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); } // Output: 3, 3, 3 (after 1 second) // Why: var is function-scoped, closures capture the same variable // Fixed versions: // With let for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); } // Output: 0, 1, 2 // With IIFE for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 1000); })(i); } // Output: 0, 1, 2
Question 2: Object property access
const obj = { a: 1, b: 2, c: { a: 3, b: 4 } }; const { a, b, c: { a: nestedA } } = obj; console.log(a, b, nestedA); // 1, 2, 3 // What happens here? const key = 'a'; console.log(obj.key); // undefined (looks for literal 'key' property) console.log(obj[key]); // 1 (evaluates variable)
Question 3: Function behavior
function test() { console.log(arguments.length); console.log(typeof arguments); console.log(Array.isArray(arguments)); } test(1, 2, 3, 4, 5); // Output: 5, "object", false // arguments is array-like but not a real array // Modern equivalent with rest parameters function modernTest(...args) { console.log(args.length); console.log(typeof args); console.log(Array.isArray(args)); } modernTest(1, 2, 3, 4, 5); // Output: 5, "object", true
Question 4: Scope and closures
let x = 1; function outer() { let x = 2; function inner() { console.log(x); // What does this log? } return inner; } const fn = outer(); fn(); // Logs: 2 (lexical scope, not dynamic scope) // Even after outer() finished, inner() remembers its scope
Tricky Array Methods
// map vs forEach return behavior const numbers = [1, 2, 3]; // forEach returns undefined const result1 = numbers.forEach(n => n * 2); console.log(result1); // undefined // map returns new array const result2 = numbers.map(n => n * 2); console.log(result2); // [2, 4, 6] // sort() mutates original array and returns it const arr = [3, 1, 4, 1, 5]; const sorted = arr.sort(); console.log(arr); // [1, 1, 3, 4, 5] - mutated! console.log(sorted); // [1, 1, 3, 4, 5] - same reference // Gotcha: sort() converts to strings by default const nums = [1, 10, 2, 20]; nums.sort(); console.log(nums); // [1, 10, 2, 20] - not [1, 2, 10, 20]! // Fix with compare function nums.sort((a, b) => a - b); console.log(nums); // [1, 2, 10, 20]
Object Reference Gotchas
const a = { value: 1 }; const b = a; const c = { value: 1 }; console.log(a === b); // true (same reference) console.log(a === c); // false (different references) console.log(a.value === c.value); // true (same values) // Mutation through references b.value = 2; console.log(a.value); // 2 (a and b point to same object) // Object.assign creates shallow copy const original = { a: 1, nested: { b: 2 } }; const copy = Object.assign({}, original); copy.a = 10; copy.nested.b = 20; console.log(original.a); // 1 (not affected) console.log(original.nested.b); // 20 (affected! shallow copy)
Promise Chain Gotchas
// What gets logged? Promise.resolve(1) .then(val => { console.log(val); // 1 return val + 1; }) .then(val => { console.log(val); // 2 // No return statement! }) .then(val => { console.log(val); // undefined }); // Error handling gotcha Promise.reject("error") .then(val => console.log("Success:", val)) .catch(err => { console.log("Caught:", err); // "Caught: error" return "recovered"; }) .then(val => { console.log("After catch:", val); // "After catch: recovered" });
Advanced Gotchas
parseInt surprises
console.log(parseInt("08")); // 8 console.log(parseInt("08", 10)); // 8 console.log(parseInt("08", 8)); // 0 (invalid octal) ["1", "2", "3"].map(parseInt); // What do you think this returns? // [1, NaN, NaN] // Because: parseInt(string, radix) // "1", 0 -> parseInt("1", 0) -> 1 // "2", 1 -> parseInt("2", 1) -> NaN (radix 1 is invalid) // "3", 2 -> parseInt("3", 2) -> NaN (3 is not valid in base 2) // Fix: ["1", "2", "3"].map(str => parseInt(str, 10)); // [1, 2, 3]
Date constructor gotchas
// Month is 0-indexed! console.log(new Date(2023, 1, 15)); // February 15, 2023 (not January!) // Different behavior for Date.parse vs constructor console.log(new Date("2023-01-15")); // Local timezone console.log(new Date("2023-01-15T00:00:00")); // Local timezone console.log(new Date("2023-01-15T00:00:00Z")); // UTC
Number precision issues
console.log(0.1 + 0.2); // 0.30000000000000004 console.log(0.1 + 0.2 === 0.3); // false // Fix with proper comparison function isEqual(a, b, tolerance = Number.EPSILON) { return Math.abs(a - b) < tolerance; } console.log(isEqual(0.1 + 0.2, 0.3)); // true
Next Chapter: Chapter 14: Modern JavaScript Features (ES6+)
Previous Chapter: Chapter 12: Advanced Concepts and Modern Features
Table of Contents: JavaScript Guide
