JavaScript Flashcards
What is the difference between == and === in JavaScript?
Triple equals is strict, meaning that a string with the value of true, will not equal a boolean with true.
Double equals is a general equation.
What are the different types of scope in JavaScript?
Global Scope
- Variables declared outside of any function or block are in the global scope.
Function Scope
- Variables declared with var inside a function are scoped to that function.
Block Scope
- Variables declared with let or const are scoped to the nearest enclosing {} block.
Lexical Scope
- Scope is determined by the physical location of code in the source—i.e., how functions are nested.
How does hoisting work with var, let, and const?
Hoisting is a behavior in JavaScript where variable and function declarations are moved to the top of their scope (before code execution), but only the declarations are hoisted—not initializations.
- var declarations are hoisted to the top of their function or global scope
- let and const are also hoisted to the top of their block scope, but they are not initialized
What are closures and how are they used in real-world scenarios?
Closures let functions access variables from their outer scope even after that scope has finished running.
They’re used for private state, callbacks, function factories, and memoization.
Explain the difference between function declarations and arrow functions.
Function declarations are hoisted, meaning you can call them before they appear in the code, and they have their own this context. Arrow functions are not hoisted and use the this value from the surrounding (lexical) scope. Arrow functions also don’t have their own arguments object or super.
How does the JavaScript event loop work?
The event loop is what allows JavaScript (which is single-threaded) to handle asynchronous operations like setTimeout, promises, or DOM events. It works by pulling tasks from the call stack and message queue (or microtask queue) in order, ensuring non-blocking behavior.
Synchronous code runs first, then microtasks (like .then()), and finally macrotasks (like setTimeout) are processed in the order they arrive.
What are call stack, event queue, and microtask queue?
The call stack is a data structure that keeps track of function calls in a program. When a function is invoked, it’s added to the stack, and once it finishes, it’s removed.
The event queue (or task queue) holds messages like user events or setTimeout callbacks that are waiting to be processed by the JavaScript runtime. The event loop takes tasks from this queue and pushes them onto the call stack when it’s empty.
The microtask queue is similar but prioritized higher than the event queue. It contains tasks like Promise callbacks and MutationObserver, which are executed right after the current task completes and before the next event from the event queue is processed.
What is memory leak in JavaScript and how can you prevent it?
A memory leak in JavaScript occurs when a program holds onto memory it no longer needs, preventing the garbage collector from reclaiming it. This can lead to performance issues as memory usage grows over time. Common causes include forgotten timers, closures holding references, global variables, and detached DOM elements.
To prevent memory leaks, avoid using unnecessary global variables, clear timers or intervals when they’re no longer needed, break circular references, and make sure to remove event listeners and DOM nodes properly when they’re not in use. Using tools like Chrome DevTools can help detect and diagnose memory leaks.
How does garbage collection work in JavaScript?
Garbage collection in JavaScript is an automatic process that reclaims memory used by objects no longer needed by the program. The most common strategy is mark-and-sweep, where the garbage collector marks all reachable objects starting from roots (like global variables), then sweeps away everything not marked.
An object is considered reachable if it can be accessed directly or indirectly from the roots. If it’s not reachable, it’s assumed to be no longer needed and is removed from memory. Developers don’t manage memory directly but can write cleaner code to help the garbage collector do its job efficiently.
What’s the difference between synchronous and asynchronous code execution?
Synchronous code is executed line by line, where each operation waits for the previous one to complete before moving on. This can block the main thread if an operation takes a long time, like a network request or heavy computation.
Asynchronous code, on the other hand, allows certain tasks to run in the background without blocking the main thread. It uses mechanisms like callbacks, promises, and async/await to handle operations that take time, enabling smoother and more responsive programs
What is prototypal inheritance, and how is it different from classical inheritance?
Prototypal inheritance in JavaScript means objects can inherit properties and methods directly from other objects using a prototype chain. Every object has an internal link to another object called its prototype, and it can access properties from it if they’re not found on itself.
Classical inheritance, common in languages like Java or C++, uses classes and inheritance hierarchies where instances are created from class blueprints. In contrast, prototypal inheritance is more flexible, allowing dynamic extension and object-to-object inheritance without needing classes, though ES6 introduced class syntax to mimic classical inheritance.
How do Object.create() and Object.assign() work?
Object.create() creates a new object with the specified prototype object and optionally adds properties. It’s commonly used for setting up inheritance by directly linking one object to another as its prototype.
Object.assign() copies enumerable properties from one or more source objects to a target object. It’s often used for shallow cloning or merging objects, but it does not copy non-enumerable properties or deep structures like nested objects.
What is the difference between deep copy and shallow copy in JavaScript?
A shallow copy creates a new object but only copies references for nested objects or arrays, not their actual content. So if the original object has nested structures, changes to those will reflect in the copy.
A deep copy duplicates everything, including nested objects and arrays, creating entirely independent copies. This means changes to the original object or its nested elements won’t affect the deep copy. Deep copies can be made using methods like structuredClone() or libraries like Lodash’s cloneDeep().
What are the different ways to clone an object in JavaScript?
You can clone an object in JavaScript using Object.assign({}, obj) or the spread operator { …obj } for a shallow copy. For a deep copy, you can use structuredClone(obj), JSON.parse(JSON.stringify(obj)) (with limitations), or libraries like Lodash’s _.cloneDeep(). Deep copies handle nested structures, while shallow copies only duplicate the top level.
How can you flatten a deeply nested array without using .flat()?
To flatten a deeply nested array without using .flat(), you can use a recursive function. The function iterates over each element in the array, and if it finds a nested array, it recursively flattens it.
Non-array items are directly added to the result. This method works for arrays of any depth and allows for full control over the flattening process.
What’s the difference between async/await and .then()?
The primary difference between async/await and .then() lies in how they handle asynchronous code.
Async/await allows you to write asynchronous code in a synchronous-like manner, making it more readable and easier to debug. You define an async function, and use await to pause execution until a Promise resolves.
.then() is a method used with Promises. It chains callbacks to handle the result of a Promise when it resolves. While it works fine, the chaining can become harder to manage with complex or multiple async operations (callback hell).
How does error handling work with async/await?
Error handling with async/await is done using try/catch blocks. When using await, if a Promise is rejected, it will throw an error, which can be caught in the catch block. This makes the code look more like synchronous error handling and is easier to manage.
What is the difference between Promise.all, Promise.allSettled, and Promise.race?
- Promise.all waits for all promises to resolve, and returns an array of results. If any promise rejects, the entire operation fails immediately with that rejection.
- Promise.allSettled waits for all promises to either resolve or reject, returning an array with the status (resolved or rejected) of each promise, regardless of their outcomes.
- Promise.race returns the result of the first promise that resolves or rejects. Whichever promise settles first will determine the result, and the other promises are ignored.
Each method is useful depending on whether you need all promises to succeed, track all results, or just care about the first result.
How can you create a custom Promise wrapper?
You can use this customPromise like a regular Promise, providing an executor function to define how to resolve or reject it. The custom wrapper allows you to add extra functionality, such as logging, timeouts, or custom behavior before resolving or rejecting the promise.
What are the common pitfalls of using async functions inside loops?
Using async functions inside loops can lead to several common pitfalls:
- Concurrency issues: By default, async functions inside loops run concurrently, which can lead to race conditions or unexpected results if the order of execution matters. You might expect each iteration to complete sequentially, but they may run out of order.
- Promise handling: When using async functions in a loop, each iteration returns a promise, but if you don’t handle them properly (e.g., using await or Promise.all()), it can lead to unhandled promises, resulting in unexpected behavior or unhandled rejections.
- Performance problems: If you have a large number of iterations, running async functions in parallel can cause performance bottlenecks, especially if you’re making network requests or database queries. Limiting concurrency or using Promise.all() carefully can help mitigate this.
To avoid these pitfalls, consider using for…of with await for sequential execution or Promise.all() for concurrent execution with proper error handling.
What are the key differences between var, let, and const?
The key differences between var, let, and const are in their scoping and reassignment rules. var has function-level scope, while let and const are block-scoped. var is hoisted and initialized with undefined, whereas let and const are hoisted but not initialized until their declaration, causing a “temporal dead zone.”
Additionally, const prevents reassignment, while let allows it, and var can be reassigned as well.
Explain destructuring and its benefits with examples.
Destructuring is a feature in JavaScript that allows you to unpack values from arrays or objects into distinct variables. It makes your code cleaner, more readable, and reduces the need for repetitive assignments.
Example for arrays:
const arr = [1, 2, 3];
const [a, b] = arr; // a = 1, b = 2
Example for objects:
const obj = { name: ‘John’, age: 25 };
const { name, age } = obj; // name = ‘John’, age = 25
The benefits of destructuring include simplifying complex data access, reducing code verbosity, and making it easier to extract specific properties from arrays or objects.
What are template literals and how do they work?
Template literals in JavaScript allow you to create strings with embedded expressions and support multi-line strings. They are enclosed in backticks (`) and allow variables or expressions to be inserted using ${} syntax. For example, you can easily include values in a string: const greeting = Hello, ${name}!``. This feature makes string manipulation more readable and less error-prone compared to traditional string concatenation.
What are rest and spread operators, and how are they different?
The rest operator (…) is used to collect multiple values into a single array, typically in function parameters. It allows you to handle an arbitrary number of arguments.
The spread operator is used to unpack elements from an array or object, spreading them into individual items. While the rest operator gathers values, the spread operator disperses them, such as in copying or merging arrays.