JavaScript Interview Guide Part 3: Events, Promises, and Modern JavaScript

JavaScript Interview Guide Part 3: Events, Promises, and Modern JavaScript

·

5 min read

  • //continuation from part 2

21. Array Dispatch Event on Push

Interviewer: "How would you create an array that emits events whenever elements are pushed to it?"

Candidate: "I'll create a custom array class that extends the built-in Array and dispatches events:

class ObservableArray extends Array {
    constructor(...args) {
        super(...args);
        this.eventTarget = new EventTarget();
    }

    push(...items) {
        const result = super.push(...items);
        const event = new CustomEvent('push', { 
            detail: { items, newLength: this.length }
        });
        this.eventTarget.dispatchEvent(event);
        return result;
    }

    addEventListener(type, callback) {
        this.eventTarget.addEventListener(type, callback);
    }
}

// Usage
const arr = new ObservableArray();
arr.addEventListener('push', (e) => {
    console.log('Added items:', e.detail.items);
    console.log('New length:', e.detail.newLength);
});

arr.push(1, 2, 3);

This pattern is useful for reactive programming scenarios where you need to respond to data changes."

22. Deep Clone Object

Interviewer: "How would you implement a deep clone function that handles circular references?"

Candidate: "Here's a solution using a WeakMap to handle circular references:

function deepClone(obj, visited = new WeakMap()) {
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }

    if (visited.has(obj)) {
        return visited.get(obj);
    }

    const clone = Array.isArray(obj) ? [] : {};
    visited.set(obj, clone);

    Object.entries(obj).forEach(([key, value]) => {
        clone[key] = deepClone(value, visited);
    });

    return clone;
}

// Example usage
const original = {
    name: 'John',
    address: { city: 'New York' }
};
original.self = original; // Circular reference

const cloned = deepClone(original);

The WeakMap prevents infinite recursion with circular references while properly copying all nested structures."

23. JSON Stringify Implementation

Interviewer: "Can you implement a basic version of JSON.stringify?"

Candidate: "I'll implement a simplified version that handles common types:

function customStringify(value) {
    // Handle primitive types
    if (value === null) return 'null';
    if (typeof value === 'string') return `"${value}"`;
    if (typeof value === 'number' || typeof value === 'boolean') {
        return String(value);
    }

    // Handle arrays
    if (Array.isArray(value)) {
        const elements = value.map(item => 
            item === undefined ? 'null' : customStringify(item)
        );
        return `[${elements.join(',')}]`;
    }

    // Handle objects
    if (typeof value === 'object') {
        const pairs = Object.entries(value)
            .filter(([_, val]) => val !== undefined)
            .map(([key, val]) => 
                `"${key}":${customStringify(val)}`
            );
        return `{${pairs.join(',')}}`;
    }
}

// Example
const obj = {
    name: 'John',
    age: 30,
    hobbies: ['reading', null, undefined]
};
console.log(customStringify(obj));

This covers the main functionality of JSON.stringify, though the real implementation has additional features like the replacer function."

24. React DOM Render

Interviewer: "Can you explain how React's DOM rendering works and implement a simple version?"

Candidate: "I'll create a basic version that handles elements and text nodes:

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.flat()
        }
    };
}

function render(element, container) {
    // Handle text nodes
    if (typeof element === 'string' || typeof element === 'number') {
        container.appendChild(document.createTextNode(element));
        return;
    }

    // Create DOM element
    const dom = document.createElement(element.type);

    // Add properties
    Object.entries(element.props || {})
        .filter(([key]) => key !== 'children')
        .forEach(([key, value]) => {
            if (key.startsWith('on')) {
                dom.addEventListener(key.toLowerCase().slice(2), value);
            } else {
                dom.setAttribute(key, value);
            }
        });

    // Render children
    element.props.children.forEach(child => render(child, dom));

    container.appendChild(dom);
}

// Usage
const element = createElement('div', { class: 'container' },
    createElement('h1', null, 'Hello'),
    createElement('p', null, 'World')
);

render(element, document.getElementById('root'));

This is a simplified version that shows the core concepts of how React creates and updates DOM elements."

25. Retry Promise N Times

Interviewer: "Implement a function that retries a promise a specified number of times before failing."

Candidate: "Here's a solution with exponential backoff:

async function retryPromise(fn, retries, delay = 1000, multiplier = 2) {
    try {
        return await fn();
    } catch (error) {
        if (retries === 0) throw error;

        await new Promise(resolve => setTimeout(resolve, delay));

        return retryPromise(
            fn,
            retries - 1,
            delay * multiplier,
            multiplier
        );
    }
}

// Usage
const fetchWithRetry = () => 
    retryPromise(
        () => fetch('https://api.example.com/data'),
        3,  // retry 3 times
        1000 // start with 1s delay
    );

This is particularly useful for handling transient failures in network requests or other unreliable operations."

26. Extended Event Emitter

Interviewer: "Can you implement an event emitter with support for wildcards and once listeners?"

Candidate: "Here's an enhanced event emitter implementation:

class EventEmitter {
    constructor() {
        this.events = new Map();
    }

    on(event, callback) {
        if (!this.events.has(event)) {
            this.events.set(event, new Set());
        }
        this.events.get(event).add(callback);
        return () => this.off(event, callback);
    }

    once(event, callback) {
        const wrapper = (...args) => {
            callback(...args);
            this.off(event, wrapper);
        };
        return this.on(event, wrapper);
    }

    emit(event, ...args) {
        const callbacks = this.events.get(event);
        if (callbacks) {
            callbacks.forEach(cb => cb(...args));
        }

        // Handle wildcards
        if (event !== '*') {
            this.emit('*', event, ...args);
        }
    }

    off(event, callback) {
        const callbacks = this.events.get(event);
        if (callbacks) {
            callbacks.delete(callback);
        }
    }
}

// Usage
const emitter = new EventEmitter();
const unsubscribe = emitter.once('user:login', user => {
    console.log('User logged in:', user);
});

This implementation supports both one-time listeners and wildcard events, making it more flexible for complex applications."

27. Promise.all Implementation

Interviewer: "Can you implement your own version of Promise.all?"

Candidate: "Here's an implementation that maintains the same behavior as the native Promise.all:

function promiseAll(promises) {
    return new Promise((resolve, reject) => {
        const results = [];
        let completed = 0;

        if (promises.length === 0) {
            resolve(results);
            return;
        }

        promises.forEach((promise, index) => {
            Promise.resolve(promise)
                .then(value => {
                    results[index] = value;
                    completed++;

                    if (completed === promises.length) {
                        resolve(results);
                    }
                })
                .catch(reject);
        });
    });
}

// Usage
const promises = [
    Promise.resolve(1),
    Promise.resolve(2),
    new Promise(resolve => setTimeout(() => resolve(3), 1000))
];

promiseAll(promises).then(console.log);

This implementation maintains the key features of Promise.all:

  • Resolves when all promises resolve

  • Rejects immediately if any promise rejects

  • Preserves the order of results"

Remember in interviews to:

  • Discuss edge cases

  • Talk about potential optimizations

  • Mention real-world applications

  • Be ready to handle follow-up questions about error handling and edge cases

This completes Part 3 of our JavaScript interview guide. Stay tuned for Part 4 where we'll cover more advanced patterns and optimizations!