Back to Articles

Chapter 13: The Interview Gauntlet - Common Tricks and Gotchas

July 27, 202512 min read
javascriptinterviewgotchastype-coercionthis-bindingevent-loopdebugging
Chapter 13: The Interview Gauntlet - Common Tricks and Gotchas

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