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! 🚀