25 Advanced Vanilla JavaScript Coding Questions
JavaScript Developer WorldWide
Join the JavaScript Developers worldwide JavaScript Developers JavaScript Coders JavaScript Freelancers
JavaScript has evolved dramatically over the years. Beyond the basics lie powerful, nuanced topics that are essential for developing efficient, scalable, and robust applications. In this post, we explore 25 advanced JavaScript coding questions, each accompanied by an in-depth explanation and code examples to illustrate key concepts.
1. How Does the JavaScript Event Loop Handle Microtasks vs. Macrotasks?
Answer: The event loop in JavaScript manages asynchronous tasks using two queues: the microtask queue (for promises, MutationObserver callbacks, etc.) and the macrotask queue (for setTimeout, setInterval, I/O events, etc.). After executing the current stack, the event loop first processes all microtasks before moving to the next macrotask. This prioritization ensures that promise callbacks run as soon as possible.
Example:
console.log('Start');
setTimeout(() => console.log('Macrotask'), 0);
Promise.resolve().then(() => console.log('Microtask'));
console.log('End');
// Output order: Start, End, Microtask, Macrotask
2. What Are Memory Leaks in JavaScript and How Can You Prevent Them?
Answer: Memory leaks occur when unused memory isn’t released. Common causes include global variables, forgotten timers or intervals, detached DOM nodes, and unintended closures that keep references alive. Prevent leaks by cleaning up timers, removing event listeners when elements are removed, and using tools (like Chrome DevTools) to profile memory usage.
3. Explain Prototypal Inheritance in Depth
Answer: JavaScript’s inheritance is based on prototypes. Each object has a hidden internal property (often accessible via proto) that points to its prototype. When a property or method is accessed, JavaScript looks up the prototype chain until it finds the property or reaches the end. Object.create() creates a new object with a specified prototype, making prototypal inheritance explicit.
Example:
const parent = {
??greet() {
????return Hello from ${this.name};
??}
};
const child = Object.create(parent);
child.name = 'Child';
console.log(child.greet()); // "Hello from Child"
4. What Are the Differences Between call, apply, and bind?
Answer:
Example:
function showDetails(age, city) {
??return ${this.name} is ${age} years old and lives in ${city}.;
}
const person = { name: "Alice" };
console.log(showDetails.call(person, 30, "New York"));
console.log(showDetails.apply(person, [30, "New York"]));
const boundShowDetails = showDetails.bind(person, 30);
console.log(boundShowDetails("Los Angeles"));
5. How Does JavaScript's Garbage Collection Work?
Answer: JavaScript uses a mark-and-sweep algorithm for garbage collection. The engine marks all objects that are reachable from the root (global scope, current execution context, etc.) and then sweeps away objects that aren’t marked, freeing memory. Understanding object references and scope is crucial to avoiding memory leaks.
6. What Are the Nuances of the this Keyword and Arrow Functions?
Answer: The this keyword refers to the execution context. Arrow functions don’t have their own this; they inherit it lexically from the surrounding scope. This behavior is useful in callback functions where maintaining the original context is necessary.
Example:
const obj = {
??name: "Arrow Function Demo",
??regularFunction: function() {
????console.log(this.name);
??},
??arrowFunction: () => {
????// 'this' here is inherited from the global scope
????console.log(this.name);
??}
};
obj.regularFunction(); // "Arrow Function Demo"
obj.arrowFunction(); ? // undefined (or global name if defined)
7. What Is the Temporal Dead Zone (TDZ) in JavaScript?
Answer: TDZ refers to the period between entering a block and the variable’s declaration when using let or const. Accessing the variable in this zone results in a ReferenceError. This behavior enforces proper declaration before usage.
Example:
{
??// console.log(temp); // ReferenceError: Cannot access 'temp' before initialization
??let temp = "TDZ";
??console.log(temp); // "TDZ"
}
8. How Do Synchronous and Asynchronous Execution Differ in JavaScript?
Answer: Synchronous code executes sequentially, blocking subsequent operations until completion. Asynchronous code, on the other hand, allows other operations to run while waiting for tasks (like API calls or timers) to complete. This is crucial for non-blocking I/O and smooth user experiences in web applications.
9. What Are Symbols and How Do They Enhance Object Properties?
Answer: Symbols are unique and immutable primitive values used as identifiers for object properties. They help prevent naming collisions, especially in large codebases or when integrating multiple libraries, by providing a unique key for each property.
Example:
const sym = Symbol('unique');
const obj = { [sym]: 'value' };
console.log(obj[sym]); // "value"
10. What Is the Reflect API and How Do Proxies Work in JavaScript?
Answer: The Reflect API provides methods for interceptable JavaScript operations (like getting, setting, or deleting properties) that mirror the behavior of internal methods. Proxies allow you to intercept and redefine fundamental operations for an object. They’re used for logging, validation, or creating custom behaviors.
Example:
const target = { name: "John" };
const handler = {
??get(target, property) {
????console.log(`Accessing ${property}`);
????return target[property];
??}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: "Accessing name", then "John"
11. How Would You Implement a Custom Event Emitter in Vanilla JavaScript?
Answer: A custom event emitter allows objects to subscribe to and emit events. You can implement this using closures to maintain a registry of events and their listeners.
Example:
function EventEmitter() {
??const events = {};
??this.on = function(event, listener) {
????if (!events[event]) {
??????events[event] = [];
????}
????events[event].push(listener);
??};
??this.emit = function(event, ...args) {
????if (events[event]) {
??????events[event].forEach(listener => listener(...args));
????}
??};
}
// Usage:
const emitter = new EventEmitter();
emitter.on("data", data => console.log("Received:", data));
emitter.emit("data", "Hello World!");
12. What Is the Module Pattern and the Revealing Module Pattern?
Answer: The Module Pattern encapsulates related functions, variables, and state into a single unit with a public API, avoiding global scope pollution. The Revealing Module Pattern improves this by returning an object that exposes only the methods you choose, keeping internal details private.
Example:
const myModule = (function() {
??let privateVar = "I am private";
??function privateMethod() {
????console.log(privateVar);
??}
??function publicMethod() {
????privateMethod();
??}
??return {
????publicMethod
??};
})();
myModule.publicMethod(); // "I am private"
13. How Can You Implement Dependency Injection in JavaScript?
Answer: Dependency injection involves passing required dependencies into a function or module instead of hardcoding them. This approach promotes loose coupling and easier testing.
Example:
function createUserService(httpClient) {
??return {
????getUser(id) {
??????return httpClient.get(`/users/${id}`);
????}
??};
}
// Inject a mock or real HTTP client:
const userService = createUserService(fetch);
userService.getUser(1);
14. What Are WeakMap and WeakSet and When Should You Use Them?
Answer: WeakMap and WeakSet allow you to store weakly referenced objects, meaning they do not prevent garbage collection if there are no other references. Use them for caching or tracking metadata where you do not want to interfere with garbage collection.
Example:
let wm = new WeakMap();
let obj = {};
wm.set(obj, "metadata");
obj = null; // Now eligible for garbage collection
15. Explain Function Currying and Partial Application in Detail
Answer: Currying transforms a function with multiple arguments into a sequence of functions, each taking a single argument. Partial application involves fixing a few arguments of a function, producing another function of smaller arity. Both techniques enable reusability and modularity.
Example (Currying):
function curriedSum(a) {
??return function(b) {
????return a + b;
??}
}
const addFive = curriedSum(5);
console.log(addFive(10)); // 15
16. What Is Tail Call Optimization and Does JavaScript Support It?
Answer: Tail call optimization (TCO) is a technique where the last function call in a recursive function is optimized to reuse the current stack frame. While ES6 introduced proper tail calls, support in JavaScript engines remains inconsistent, so reliance on TCO is not widespread.
Example:
function factorial(n, acc = 1) {
??if (n <= 1) return acc;
??return factorial(n - 1, n * acc); // Tail call
}
console.log(factorial(5)); // 120
17. What Are Async Iterators and Generators?
Answer: Generators are functions that can pause execution (yield) and resume later. Async iterators extend this concept to asynchronous operations using for await...of loops, allowing you to handle data streams or asynchronous sequences elegantly.
Example (Generator):
function* countGenerator() {
??yield 1;
??yield 2;
??yield 3;
}
for (let value of countGenerator()) {
??console.log(value);
}
18. How Would You Implement a Custom Promise from Scratch?
Answer: Building a Promise involves managing states (pending, fulfilled, rejected), storing callbacks, and ensuring proper asynchronous execution. While simplified versions exist for educational purposes, creating a production-level promise requires rigorous handling of edge cases.
Example (Simplified):
function MyPromise(executor) {
??let onResolve, onReject;
??let fulfilled = false, rejected = false, value;
??function resolve(val) {
????fulfilled = true;
????value = val;
????if (typeof onResolve === 'function') {
??????onResolve(value);
????}
??}
??function reject(reason) {
????rejected = true;
????value = reason;
????if (typeof onReject === 'function') {
??????onReject(value);
????}
??}
??this.then = function(callback) {
????onResolve = callback;
????if (fulfilled) onResolve(value);
????return this;
??};
??this.catch = function(callback) {
????onReject = callback;
????if (rejected) onReject(value);
????return this;
??};
??executor(resolve, reject);
}
// Usage:
const promise = new MyPromise((resolve, reject) => {
??setTimeout(() => resolve("Done"), 1000);
});
promise.then(result => console.log(result));
19. How Do Microtasks and Macrotasks Differ in Node.js (e.g., process.nextTick)?
Answer: In Node.js, process.nextTick queues a microtask that executes before the event loop continues, even before promise callbacks. Macrotasks (like setTimeout) are queued for the next cycle. Understanding these differences is key for performance tuning and avoiding starvation of I/O tasks.
20. What Are Some Methods to Deep Clone Objects Beyond JSON.parse/stringify?
Answer: Besides the JSON approach, you can use recursive functions, the structured clone algorithm (if available via structuredClone()), or libraries like Lodash’s cloneDeep that handle functions, dates, maps, and more complex types.
21. How Would You Advancedly Implement Debounce and Throttle Functions?
Answer: Building robust debounce and throttle functions involves careful handling of closures, timers, and ensuring proper context (this) preservation. Advanced implementations allow configuration for immediate execution, trailing invocation, and cancellation of pending executions.
Example (Advanced Debounce):
function debounce(fn, delay, immediate = false) {
??let timeout;
??return function(...args) {
????const context = this;
????const later = () => {
??????timeout = null;
??????if (!immediate) fn.apply(context, args);
????};
????const callNow = immediate && !timeout;
????clearTimeout(timeout);
????timeout = setTimeout(later, delay);
????if (callNow) fn.apply(context, args);
??};
}
22. How Do You Implement the Observer Pattern in JavaScript?
Answer: The Observer pattern involves subjects maintaining a list of observers, notifying them when changes occur. This pattern is useful for creating reactive systems.
Example:
function Subject() {
??this.observers = [];
}
Subject.prototype.subscribe = function(observer) {
??this.observers.push(observer);
};
Subject.prototype.notify = function(message) {
??this.observers.forEach(observer => observer.update(message));
};
function Observer(name) {
??this.name = name;
}
Observer.prototype.update = function(message) {
??console.log(`${this.name} received: ${message}`);
};
// Usage:
const subject = new Subject();
const obs1 = new Observer("Observer1");
const obs2 = new Observer("Observer2");
subject.subscribe(obs1);
subject.subscribe(obs2);
subject.notify("Hello Observers!");
23. How Can You Create a Simple Virtual DOM Implementation?
Answer: A simple virtual DOM involves representing DOM elements as plain JavaScript objects, diffing these objects to identify changes, and then updating the real DOM accordingly. While real-world virtual DOMs (like React’s) are complex, a basic version demonstrates key concepts.
Example (Conceptual):
function createElement(tag, props, ...children) {
??return { tag, props: props || {}, children };
}
function render(vdom) {
??const el = document.createElement(vdom.tag);
??Object.entries(vdom.props).forEach(([key, value]) => el.setAttribute(key, value));
??vdom.children.forEach(child => {
????const childEl = typeof child === "object" ? render(child) : document.createTextNode(child);
????el.appendChild(childEl);
??});
??return el;
}
24. What Are Advanced Uses of Destructuring and the Rest/Spread Operators?
Answer: Advanced destructuring allows you to extract values from nested objects or arrays, rename variables, and set defaults. The rest and spread operators can merge or split arrays and objects in deep copies, making code more concise and expressive.
Example (Nested Destructuring):
const user = {
??name: "Eve",
??address: {
????city: "Wonderland",
????zip: 12345
??}
};
const { name, address: { city, zip } } = user;
console.log(name, city, zip);
25. How Do You Handle Advanced Error Handling with Custom Error Classes?
Answer: Creating custom error classes by extending the built-in Error class provides more descriptive error messages and consistent error handling. Custom errors are useful for differentiating between various failure modes in an application.
Example:
class ValidationError extends Error {
??constructor(message) {
????super(message);
????this.name = "ValidationError";
??}
}
function validateUser(user) {
??if (!user.name) {
????throw new ValidationError("User must have a name");
??}
}
try {
??validateUser({});
} catch (error) {
??if (error instanceof ValidationError) {
????console.error("Validation error:", error.message);
??} else {
????console.error("Unknown error:", error);
??}
}
These 25 advanced questions cover a wide range of topics—from asynchronous patterns and memory management to deep object manipulation and design patterns in vanilla JavaScript. Understanding these concepts will not only improve your code quality but also equip you with the tools to build more sophisticated applications.?
Senior Full-Stack Software Engineer | Python, Nest.js, Next.js, React | Scalable & High-Performance Systems
6 天前Great detailed explanation! Well done