JavaScript Interview Guide Part 2: Mastering Technical Interviews

JavaScript Interview Guide Part 2: Mastering Technical Interviews

·

6 min read


  • //continuation from part 1

11. Debouncing with Leading and Trailing Edge

Interviewer: "Can you explain and implement a debounce function that supports both leading and trailing execution?"

Candidate: "Yes, let me break this down. Leading edge execution means the function runs immediately, while trailing edge means it runs after the delay. Here's how I'd implement it:

function debounce(func, wait, options = { leading: false, trailing: true }) {
    let timeout;

    return function(...args) {
        const shouldCallNow = options.leading && !timeout;

        clearTimeout(timeout);

        timeout = setTimeout(() => {
            timeout = null;
            if (options.trailing) func.apply(this, args);
        }, wait);

        if (shouldCallNow) func.apply(this, args);
    };
}

In real-world applications, we'd use this for things like search inputs where we might want an immediate response (leading) or wait for the user to finish typing (trailing)."

12. MapLimit Function

Interviewer: "How would you implement a function that processes array items with a concurrency limit?"

Candidate: "I'll implement a mapLimit function that processes n items at a time:

async function mapLimit(items, limit, asyncFn) {
    const results = [];
    const inProgress = new Set();

    for (let i = 0; i < items.length; i++) {
        if (inProgress.size >= limit) {
            await Promise.race(inProgress);
        }

        const promise = asyncFn(items[i])
            .then(result => {
                results[i] = result;
                inProgress.delete(promise);
            });

        inProgress.add(promise);
    }

    await Promise.all(inProgress);
    return results;
}

This is particularly useful when dealing with API rate limits or managing system resources."

13. Cancelable Promise

Interviewer: "How would you implement a cancelable promise?"

Candidate: "Here's a straightforward implementation of a cancelable promise:

function createCancelablePromise(promise) {
    let isCanceled = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            value => isCanceled ? reject('Canceled') : resolve(value),
            error => isCanceled ? reject('Canceled') : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel: () => { isCanceled = true; }
    };
}

// Example usage
const { promise, cancel } = createCancelablePromise(fetch('api/data'));

This pattern is useful for cleanup in React components or handling long-running operations that might need to be canceled."

14. TypeHead Search with LRU Cache

Interviewer: "Can you implement a typeahead search with an LRU cache?"

Candidate: "I'll implement a simple LRU cache first, then use it for typeahead:

class LRUCache {
    constructor(capacity) {
        this.cache = new Map();
        this.capacity = capacity;
    }

    get(key) {
        if (!this.cache.has(key)) return null;

        const value = this.cache.get(key);
        this.cache.delete(key);
        this.cache.set(key, value);
        return value;
    }

    put(key, value) {
        this.cache.delete(key);
        this.cache.set(key, value);
        if (this.cache.size > this.capacity) {
            this.cache.delete(this.cache.keys().next().value);
        }
    }
}

class TypeAhead {
    constructor() {
        this.cache = new LRUCache(100);
    }

    async search(query) {
        const cached = this.cache.get(query);
        if (cached) return cached;

        const results = await fetch(`/api/search?q=${query}`);
        this.cache.put(query, results);
        return results;
    }
}

This helps reduce API calls and improves user experience by caching recent searches."

15. Document Comparison

Interviewer: "How would you implement a function to compare two documents for similarity?"

Candidate: "I'd use a simple approach based on word frequency:

function compareDocuments(doc1, doc2) {
    const words1 = doc1.toLowerCase().split(/\W+/);
    const words2 = doc2.toLowerCase().split(/\W+/);

    const freq1 = getWordFrequency(words1);
    const freq2 = getWordFrequency(words2);

    return calculateSimilarity(freq1, freq2);
}

function getWordFrequency(words) {
    return words.reduce((freq, word) => {
        freq[word] = (freq[word] || 0) + 1;
        return freq;
    }, {});
}

function calculateSimilarity(freq1, freq2) {
    const allWords = new Set([...Object.keys(freq1), ...Object.keys(freq2)]);
    let similarity = 0;

    for (const word of allWords) {
        const f1 = freq1[word] || 0;
        const f2 = freq2[word] || 0;
        similarity += Math.min(f1, f2) / Math.max(f1, f2);
    }

    return similarity / allWords.size;
}

This approach gives us a similarity score between 0 and 1."

16. Currying Implementation

Interviewer: "Can you explain currying and implement a curry function?"

Candidate: "Currying transforms a function with multiple arguments into a sequence of functions with single arguments. Here's an implementation:

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }

        return function(...moreArgs) {
            return curried.apply(this, args.concat(moreArgs));
        };
    };
}

// Example usage
function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6

This is useful for creating more reusable and composable functions in functional programming."

17. Execute Tasks in Parallel

Interviewer: "How would you implement a function to execute multiple tasks in parallel with a limit?"

Candidate: "Here's how I would implement parallel task execution with a concurrency limit:

async function executeParallel(tasks, limit) {
    const results = [];
    const executing = new Set();

    for (const [index, task] of tasks.entries()) {
        if (executing.size >= limit) {
            await Promise.race(executing);
        }

        const promise = task().then(result => {
            results[index] = result;
            executing.delete(promise);
        });

        executing.add(promise);
    }

    await Promise.all(executing);
    return results;
}

// Usage example
const tasks = [
    () => new Promise(resolve => setTimeout(() => resolve(1), 1000)),
    () => new Promise(resolve => setTimeout(() => resolve(2), 500)),
    () => new Promise(resolve => setTimeout(() => resolve(3), 2000))
];

executeParallel(tasks, 2).then(console.log);

This is particularly useful when dealing with multiple API calls or heavy computations."

18. Finding DOM Elements

Interviewer: "How would you implement a function to find all elements matching certain criteria in the DOM?"

Candidate: "I'll implement a function that can search by various criteria:

function findElements(criteria) {
    const results = [];

    function traverse(element) {
        // Check if current element matches criteria
        if (matchesCriteria(element, criteria)) {
            results.push(element);
        }

        // Traverse children
        for (const child of element.children) {
            traverse(child);
        }
    }

    function matchesCriteria(element, criteria) {
        return Object.entries(criteria).every(([key, value]) => {
            switch(key) {
                case 'class':
                    return element.classList.contains(value);
                case 'tag':
                    return element.tagName.toLowerCase() === value.toLowerCase();
                case 'attribute':
                    return element.hasAttribute(value);
                default:
                    return element[key] === value;
            }
        });
    }

    traverse(document.body);
    return results;
}

// Usage
const buttons = findElements({ tag: 'button', class: 'primary' });

This provides a flexible way to search the DOM beyond just querySelector."

19. Sort Array Implementation

Interviewer: "Can you implement a custom sorting algorithm and explain its time complexity?"

Candidate: "I'll implement quicksort, which has an average time complexity of O(n log n):

function quickSort(arr) {
    if (arr.length <= 1) return arr;

    const pivot = arr[Math.floor(arr.length / 2)];
    const left = [];
    const right = [];
    const equal = [];

    for (const element of arr) {
        if (element < pivot) left.push(element);
        else if (element > pivot) right.push(element);
        else equal.push(element);
    }

    return [...quickSort(left), ...equal, ...quickSort(right)];
}

// Handling custom comparisons
function sortBy(arr, key) {
    return quickSort(arr.slice()).sort((a, b) => {
        if (typeof a[key] === 'string') {
            return a[key].localeCompare(b[key]);
        }
        return a[key] - b[key];
    });
}

// Usage
const sorted = quickSort([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]);

The advantage of quicksort is its efficiency in practice, though it's worth noting that for small arrays, simpler algorithms might be more appropriate."

20. Object Flattening

Interviewer: "Can you write a function that flattens a nested object structure?"

Candidate: "Here's how I would implement object flattening with support for arrays and nested objects:

function flattenObject(obj, prefix = '') {
    return Object.entries(obj).reduce((acc, [key, value]) => {
        const newKey = prefix ? `${prefix}.${key}` : key;

        if (value && typeof value === 'object' && !Array.isArray(value)) {
            Object.assign(acc, flattenObject(value, newKey));
        } else {
            acc[newKey] = value;
        }

        return acc;
    }, {});
}

// Example usage
const nested = {
    name: 'John',
    address: {
        street: '123 Main St',
        city: {
            name: 'Boston',
            zip: '02101'
        }
    }
};

console.log(flattenObject(nested));
// Output:
// {
//     'name': 'John',
//     'address.street': '123 Main St',
//     'address.city.name': 'Boston',
//     'address.city.zip': '02101'
// }

This is useful when working with complex data structures that need to be simplified for storage or processing."

Remember, in an interview setting, it's important to:

  • Explain your thought process as you code

  • Mention edge cases and potential improvements

  • Discuss time and space complexity when relevant

  • Be prepared to optimize your solution if asked

This completes Part 2 of our JavaScript interview guide. Up next in Part 3, we'll cover more advanced topics including promises, async patterns, and performance optimization.