- In JavaScript, an iterator is an object that defines a sequence, and potentially a return value upon its completion.
- An iterator is any object that implements the iterator protocol by having a
next() method.
- The iterator protocol standardises how a sequence of values is produced.
- An object is considered iterable if it implements the
Symbol.iterator method.
Iterables and iterators can be easy to mix up. Here is an easy way to think about the difference:
- Iterable: An object that has a sequence of data and wants to share it (like a book with pages, for instance).
- Iterator: An object that does the work of getting the next item from that sequence (like a bookmark keeping track of where you are).
- An iterable is any JavaScript object that implements the iterable protocol.
- To be an iterable, an object must have a method with the special key
Symbol.iterator.
- When called, this method returns a new iterator.
- Arrays, Strings, Maps, Sets and NodeLists are all built-in iterables.
- Plain JavaScript objects (
{}) are not iterable by default.
Example:
- An iterator is an object that actually steps through the iterable.
- An iterator must have a
next() method.
The next() method returns an object with two properties:
value: The next value in the sequence.
done: A boolean that is true if the iterator has reached the end of the sequence or false if it has not.
When you call the next() method on an iterator, it steps through a collection one by one.
Example 1:
Example 2:
When you write a for...of loop, JavaScript automatically coordinates the iterable and iterator behind the scenes, so next() doesn't have to be called manually.
Example:
Generator functions (function*) and bidirectional communication
- Generator functions (declared with
function*) simplify writing iterators.
- Instead of manually creating a function with a
next() method, you use the yield keyword to pause execution and emit a value.
Example:
- Generators don't just output data, they can receive data back into the
yield expression through the arguments passed into next(value).
Example:
Lazy evaluation means computing values on-demand rather than ahead of time. This enables powerful design patterns like infinite streams and memory-efficient data processing.
- Because generators pause execution, you can define infinite loops without the program crashing. Values are only generated when requested.
Example:
- Generators make complex traversals clean because you can yield values recursively using
yield*.
Example:
- You can combine generators with
async/await to stream data asynchronously (e.g., fetching pages of an API lazily).
The Proxy object
A Proxy allows you to wrap an object and intercept or redefine fundamental operations (like getting a property, setting a property, validating data, or calling a function).
Example:
The Reflect API is a built-in object that provides methods for interceptable JavaScript operations. It mirrors the traps of Proxy. Whenever you intercept an operation with a Proxy, it is common practise to use Reflect to perform the default behaviour.
Example:
Modern front-end frameworks (like Vue 3) use Proxy under the hood to make application state "reactive". When a piece of data changes, the framework intercepts the write operation and automatically triggers DOM updates or re-renders components.
Example:
Proxies allow you to enforce schemas, data types, or boundaries dynamically without needing to write custom getters and setters for every single property on a class or object.
Example:
Sometimes you want to automatically initialise missing keys when you try to read or write to them, preventing undefined errors. This is especially useful for nested structures, graphs, or caching layers.
Example:
- Modern JavaScript engines (such as Google's V8) manage memory by dividing it into two primary areas:
- The stack.
- The heap.
- A structured, fast memory region operating on "Last-In-First-Out".
- It is responsible for static memory allocation, including execution contexts, pointer references and primitive values.
- Allocating and clearing the stack is extremely fast because the size of primitives is known and fixed at compile time.
- An unstructured, large memory pool used for dynamic memory allocation where data sizes can change or are unknown ahead of time (e.g.,
objects, arrays and functions).
- Allocating memory takes longer because the engine as to search for a block of memory large enough to fit the data.
- Primitive values (
string, number, boolean, null, undefined, symbol, bigint): These are stored directly on the stack. When you assign a primitive to a new variable, a completely independent copy of that value is created.
- Reference values (
object, array, function): The actual data structure is stored on the heap, whereas a reference pointer to that heap address is stored on the stack. When an object is assigned to a new variable you are only copying the pointer. Both variables point to the exact same object in the heap. Modifying one will affect the other.
- The engine runs automated processes to reclaim memory from objects that are no longer needed.
- This is the foundational algorithm used by V8.
- The algorithm goes through two phases:
- A mark phase.
- A sweep phase.
- The algorithm periodically starts from roots (global objects, currently active execution stacks/closures).
- It traverses down the reference graph, marking every object it can reach.
- During the sweep phase, any memory blocks that remain unmarked are deemed unreachable (garbage) and are reclaimed by the engine.
The heap is split into:
- The young generation (split further into "nursery" and "intermediate" generations): This is where new objects are born. It is collected frequently using a fast Scavenge algorithm (minor GC) that moves surviving objects between two semi-spaces.
- The old generation: Objects that survive multiple young generation sweeps are placed here. It is collected less frequently using a Mark-Sweep-Compact algorithm (major GC).
- Accidental globals: Forgetting to declare a variable with
const, let, or var implicitly attaches it to the global window object - meaning it will never be garbage collected.
- Forgotten event listeners: Attaching event listeners to global elements (like
window, document or large parent containers) and failing to remove them (removeEventListener) when components unmount keeps the target elements and their outer scopes trapped in memory.
- Detached DOM nodes: Storing a reference to a DOM element in a JavaScript
array or object, and later removing that element from the document tree.
- Closure retention: Inner functions that retain references to outer scope variables long after the outer function has returned can accidentally trap heavy data structures if not scoped properly.
To address memory leaks you can:
- Open Chrome DevTools and navigate to the Memory tab.
- Take a Heap Snapshot before performing an action (e.g., opening and closing a modal).
- Perform an action and take another snapshot.
- Compare snapshots using the Comparison view or filter by Detached Elements to look for DOM nodes that should have been garbage collected.
- Inspect the Retainer Tree to trace why an object is alive (which variable or listener is holding on to the reference), then you can clean up the reference in your code.
Comments (0)