JavaScript Interview Guide Part 1: Core Concepts and Function Handling

JavaScript Interview Guide Part 1: Core Concepts and Function Handling

·

6 min read

Welcome to the first part of our comprehensive JavaScript interview preparation series! Let's explore the first 10 fundamental concepts through real interview scenarios.

1. Chain Calculator Implementation

Interviewer: "Can you implement a calculator that allows for method chaining?"

Candidate: "Yes, we can create a fluent interface using method chaining. Here's the implementation:

class Calculator {
    constructor(value = 0) {
        this.value = value;
    }

    add(number) {
        this.value += number;
        return this;
    }

    subtract(number) {
        this.value -= number;
        return this;
    }

    multiply(number) {
        this.value *= number;
        return this;
    }

    divide(number) {
        if (number === 0) throw new Error('Division by zero');
        this.value /= number;
        return this;
    }

    getResult() {
        return this.value;
    }
}

// Usage
const result = new Calculator(5)
    .add(10)
    .multiply(2)
    .subtract(5)
    .divide(3)
    .getResult();
console.log(result); // 10

The key is returning this from each method to enable chaining."

2. Promises in Sequence

Interviewer: "How would you execute multiple promises in sequence rather than parallel?"

Candidate: "We can use reduce with async/await to execute promises sequentially:

async function executeSequentially(promiseFns) {
    return promiseFns.reduce(async (promise, fn) => {
        // Wait for the previous promise to complete
        await promise;
        // Execute the current promise
        return fn();
    }, Promise.resolve());
}

// Example usage
const tasks = [
    () => new Promise(resolve => setTimeout(() => resolve('First'), 1000)),
    () => new Promise(resolve => setTimeout(() => resolve('Second'), 500)),
    () => new Promise(resolve => setTimeout(() => resolve('Third'), 300))
];

executeSequentially(tasks).then(console.log);

This ensures each promise starts only after the previous one completes."

3. Pipe and Compose Functions

Interviewer: "Explain the difference between pipe and compose functions and implement both."

Candidate: "Pipe and compose both combine functions, but in different orders. Here's the implementation:

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

// Example usage
const addTwo = x => x + 2;
const multiplyByThree = x => x * 3;
const square = x => x * x;

const withPipe = pipe(addTwo, multiplyByThree, square);
const withCompose = compose(square, multiplyByThree, addTwo);

console.log(withPipe(5));  // ((5 + 2) * 3)² = 441
console.log(withCompose(5));  // ((5 + 2) * 3)² = 441

Pipe processes left-to-right, while compose processes right-to-left."

4. Array Polyfills

Interviewer: "Can you implement your own version of map and filter array methods?"

Candidate: "Here's how we can implement these array methods from scratch:

Array.prototype.myMap = function(callback) {
    const result = [];
    for (let i = 0; i < this.length; i++) {
        result.push(callback(this[i], i, this));
    }
    return result;
};

Array.prototype.myFilter = function(callback) {
    const result = [];
    for (let i = 0; i < this.length; i++) {
        if (callback(this[i], i, this)) {
            result.push(this[i]);
        }
    }
    return result;
};

// Usage example
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.myMap(x => x * 2);
const evens = numbers.myFilter(x => x % 2 === 0);

These implementations maintain the same functionality as the built-in methods."

5. Prototype and Inheritance

Interviewer: "How does prototype inheritance work in JavaScript? Can you demonstrate?"

Candidate: "Here's a practical example of prototype inheritance:

function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    return `${this.name} makes a sound`;
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add method specific to Dog
Dog.prototype.speak = function() {
    return `${this.name} barks!`;
};

const dog = new Dog('Rex', 'German Shepherd');
console.log(dog.speak()); // "Rex barks!"

This demonstrates both inheritance and method overriding."

6. Call, Apply, and Bind

Interviewer: "What's the difference between call, apply, and bind? Show practical examples."

Candidate: "Here's a demonstration of all three methods:

const person = {
    name: 'John',
    greet(greeting, punctuation) {
        return `${greeting}, I'm ${this.name}${punctuation}`;
    }
};

const anotherPerson = {
    name: 'Jane'
};

// Using call
console.log(person.greet.call(anotherPerson, 'Hi', '!')); // "Hi, I'm Jane!"

// Using apply
console.log(person.greet.apply(anotherPerson, ['Hello', '?'])); // "Hello, I'm Jane?"

// Using bind
const boundGreet = person.greet.bind(anotherPerson);
console.log(boundGreet('Hey', '.')); // "Hey, I'm Jane."

The key differences are:

  • call: Executes immediately with arguments listed out

  • apply: Executes immediately with arguments as an array

  • bind: Returns a new function with bound context"

7. Flatten Array

Interviewer: "How would you implement a function to flatten a nested array?"

Candidate: "Here are two approaches to flatten an array:

// Using recursion
function flattenRecursive(arr) {
    return arr.reduce((flat, item) => {
        return flat.concat(Array.isArray(item) ? flattenRecursive(item) : item);
    }, []);
}

// Using stack (iterative approach)
function flattenIterative(arr) {
    const stack = [...arr];
    const result = [];

    while (stack.length) {
        const next = stack.pop();
        if (Array.isArray(next)) {
            stack.push(...next);
        } else {
            result.unshift(next);
        }
    }

    return result;
}

// Test
const nested = [1, [2, 3, [4, 5]], 6];
console.log(flattenRecursive(nested)); // [1, 2, 3, 4, 5, 6]
console.log(flattenIterative(nested)); // [1, 2, 3, 4, 5, 6]

The recursive approach is cleaner but might have stack limitations for deeply nested arrays."

8. Basic Debouncing

Interviewer: "Implement a basic debounce function and explain its use cases."

Candidate: "Here's a debounce implementation:

function debounce(func, wait) {
    let timeout;

    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func.apply(this, args);
        };

        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

// Usage example
const handleSearch = debounce((event) => {
    console.log('Searching:', event.target.value);
}, 300);

searchInput.addEventListener('input', handleSearch);

This is useful for search inputs, window resize handlers, or any event that fires rapidly."

9. Basic Throttling

Interviewer: "What's the difference between throttling and debouncing? Implement throttle."

Candidate: "Here's a throttle implementation:

function throttle(func, limit) {
    let inThrottle;

    return function executedFunction(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// Usage example
const handleScroll = throttle(() => {
    console.log('Scroll position:', window.scrollY);
}, 500);

window.addEventListener('scroll', handleScroll);

While debounce waits for quiet time, throttle ensures function calls happen at a regular interval."

10. Event Emitter

Interviewer: "Can you implement a basic event emitter class?"

Candidate: "Here's a simple implementation:

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

    on(event, callback) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(callback);
        return this;
    }

    emit(event, data) {
        const callbacks = this.events[event];
        if (callbacks) {
            callbacks.forEach(callback => callback(data));
        }
        return this;
    }

    off(event, callback) {
        const callbacks = this.events[event];
        if (callbacks) {
            this.events[event] = callbacks.filter(cb => cb !== callback);
        }
        return this;
    }
}

// Usage
const emitter = new EventEmitter();
const handler = data => console.log('Event received:', data);

emitter.on('data', handler);
emitter.emit('data', 'Hello World');
emitter.off('data', handler);

This demonstrates the publisher-subscriber pattern commonly used in Node.js."

Remember to not just memorize these implementations, but understand the underlying concepts and use cases. In interviews, it's important to discuss:

  • Performance implications

  • Edge cases

  • Real-world applications

  • Potential improvements

Happy interviewing! Stay tuned for Part 2! 🚀