JavaScript Memory Model: Understanding Data Types, References, and Garbage Collection
JavaScript's handling of memory and data types is often misunderstood, leading to bugs and performance issues. This article takes an investigative approach to examine how JavaScript manages memory through practical code examples. Part 1: The Two Worlds of JavaScript Data Types Primitive vs Reference Types: A Fundamental Distinction // Primitive example let a = 5; let b = a; a = 10; console.log(a); // 10 console.log(b); // 5 // Reference example let x = [1, 2, 3]; let y = x; x.push(4); console.log(x); // [1, 2, 3, 4] console.log(y); // [1, 2, 3, 4] Investigation #1: Let's examine what's happening in memory: Primitive values are stored directly in the variable: When b = a happens, the value 5 is copied from a to b When a changes to 10, b remains unchanged Reference values store a pointer to the data: When y = x happens, both variables reference the same array When we modify the array through x, y sees those changes The Definitive List of Data Types Primitive Types: String Number Boolean Undefined Null Symbol BigInt Reference Types: Object (including Arrays, Functions, Dates, RegExps, Maps, Sets, etc.) Part 2: Diving into Reference Memory Model Behind-the-Scenes Investigation Let's probe deeper with some experiments: // Experiment 1: Objects and equality let obj1 = { name: "Alice" }; let obj2 = { name: "Alice" }; let obj3 = obj1; console.log(obj1 === obj2); // false console.log(obj1 === obj3); // true Investigation #2: Why do identical-looking objects compare as not equal? obj1 and obj2 point to different memory locations obj1 and obj3 point to the same memory location // Experiment 2: Modifying through references let person = { name: "Bob", age: 30 }; let jobProfile = { person: person, title: "Developer" }; person.age = 31; console.log(jobProfile.person.age); // 31 Investigation #3: Nested objects share references Changing person affects what you see through jobProfile.person Visualizing Memory References // Reassignment vs. modification let arr1 = [1, 2, 3]; let arr2 = arr1; // Modification (affects both references) arr1.push(4); console.log("After modification:"); console.log(arr1); // [1, 2, 3, 4] console.log(arr2); // [1, 2, 3, 4] // Reassignment (only affects one variable) arr1 = [5, 6, 7]; console.log("After reassignment:"); console.log(arr1); // [5, 6, 7] console.log(arr2); // [1, 2, 3, 4] Investigation #4: Two different operations: Modification: Changes the data both variables point to Reassignment: Makes a variable point to new data Part 3: Garbage Collection in Practice When Does Memory Get Freed? // Creating potentially unused objects function createObjects() { let tempArray = new Array(1000).fill("data"); // This object becomes unreachable after the function returns let unretainedObject = { huge: new Array(10000).fill("more data") }; // This object will be returned and retained let retainedObject = { name: "I survive" }; return retainedObject; } let survivor = createObjects(); // When does unretainedObject get garbage collected? Investigation #5: Tracing object lifecycles unretainedObject becomes eligible for GC when createObjects() returns retainedObject remains in memory because it's assigned to survivor Memory Leaks: When References Persist // Potential memory leak through closures function setupHandler() { let largeData = new Array(10000).fill("lots of data"); return function() { // This closure maintains a reference to largeData console.log("Handler using", largeData.length, "items"); }; } let handler = setupHandler(); // largeData remains in memory as long as handler exists Investigation #6: Unintended retention Even though we never directly access largeData again, it stays in memory The closure in the returned function maintains a reference Part 4: Practical Implications Copying Objects: Shallow vs. Deep Copy // Shallow copy let original = { name: "Original", details: { id: 123 } }; let shallowCopy = { ...original }; // or Object.assign({}, original) shallowCopy.name = "Changed"; shallowCopy.details.id = 456; console.log(original.name); // "Original" (primitive value was copied) console.log(original.details.id); // 456 (reference value was shared) Investigation #7: The limits of spread operator and Object.assign They only create a shallow copy Nested objects are still shared references Performance Considerations // Creating many short-lived objects function processData(items) { let results = []; for (let i = 0; i < items.length; i++) { // Each iteration creates new temporary objects let temp = { processed: items[i] * 2, original: items[i] }; results.push(temp.processed); } return results; } const data = new Array(10000).fill(0).map((_, i) => i); console.time("processing"); processData(data); console.timeEnd("processing");

JavaScript's handling of memory and data types is often misunderstood, leading to bugs and performance issues. This article takes an investigative approach to examine how JavaScript manages memory through practical code examples.
Part 1: The Two Worlds of JavaScript Data Types
Primitive vs Reference Types: A Fundamental Distinction
// Primitive example
let a = 5;
let b = a;
a = 10;
console.log(a); // 10
console.log(b); // 5
// Reference example
let x = [1, 2, 3];
let y = x;
x.push(4);
console.log(x); // [1, 2, 3, 4]
console.log(y); // [1, 2, 3, 4]
Investigation #1: Let's examine what's happening in memory:
Primitive values are stored directly in the variable:
- When b = a happens, the value 5 is copied from a to b
- When a changes to 10, b remains unchanged
Reference values store a pointer to the data:
- When y = x happens, both variables reference the same array
- When we modify the array through x, y sees those changes
The Definitive List of Data Types
-
Primitive Types:
- String
- Number
- Boolean
- Undefined
- Null
- Symbol
- BigInt
-
Reference Types:
- Object (including Arrays, Functions, Dates, RegExps, Maps, Sets, etc.)
Part 2: Diving into Reference Memory Model
Behind-the-Scenes Investigation
Let's probe deeper with some experiments:
// Experiment 1: Objects and equality
let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };
let obj3 = obj1;
console.log(obj1 === obj2); // false
console.log(obj1 === obj3); // true
Investigation #2: Why do identical-looking objects compare as not equal?
- obj1 and obj2 point to different memory locations
- obj1 and obj3 point to the same memory location
// Experiment 2: Modifying through references
let person = { name: "Bob", age: 30 };
let jobProfile = { person: person, title: "Developer" };
person.age = 31;
console.log(jobProfile.person.age); // 31
Investigation #3: Nested objects share references
Changing person affects what you see through jobProfile.person
Visualizing Memory References
// Reassignment vs. modification
let arr1 = [1, 2, 3];
let arr2 = arr1;
// Modification (affects both references)
arr1.push(4);
console.log("After modification:");
console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // [1, 2, 3, 4]
// Reassignment (only affects one variable)
arr1 = [5, 6, 7];
console.log("After reassignment:");
console.log(arr1); // [5, 6, 7]
console.log(arr2); // [1, 2, 3, 4]
Investigation #4: Two different operations:
- Modification: Changes the data both variables point to
- Reassignment: Makes a variable point to new data
Part 3: Garbage Collection in Practice
When Does Memory Get Freed?
// Creating potentially unused objects
function createObjects() {
let tempArray = new Array(1000).fill("data");
// This object becomes unreachable after the function returns
let unretainedObject = { huge: new Array(10000).fill("more data") };
// This object will be returned and retained
let retainedObject = { name: "I survive" };
return retainedObject;
}
let survivor = createObjects();
// When does unretainedObject get garbage collected?
Investigation #5: Tracing object lifecycles
- unretainedObject becomes eligible for GC when createObjects() returns
- retainedObject remains in memory because it's assigned to survivor
Memory Leaks: When References Persist
// Potential memory leak through closures
function setupHandler() {
let largeData = new Array(10000).fill("lots of data");
return function() {
// This closure maintains a reference to largeData
console.log("Handler using", largeData.length, "items");
};
}
let handler = setupHandler();
// largeData remains in memory as long as handler exists
Investigation #6: Unintended retention
- Even though we never directly access largeData again, it stays in memory
- The closure in the returned function maintains a reference
Part 4: Practical Implications
Copying Objects: Shallow vs. Deep Copy
// Shallow copy
let original = { name: "Original", details: { id: 123 } };
let shallowCopy = { ...original }; // or Object.assign({}, original)
shallowCopy.name = "Changed";
shallowCopy.details.id = 456;
console.log(original.name); // "Original" (primitive value was copied)
console.log(original.details.id); // 456 (reference value was shared)
Investigation #7: The limits of spread operator and Object.assign
- They only create a shallow copy
- Nested objects are still shared references
Performance Considerations
// Creating many short-lived objects
function processData(items) {
let results = [];
for (let i = 0; i < items.length; i++) {
// Each iteration creates new temporary objects
let temp = {
processed: items[i] * 2,
original: items[i]
};
results.push(temp.processed);
}
return results;
}
const data = new Array(10000).fill(0).map((_, i) => i);
console.time("processing");
processData(data);
console.timeEnd("processing");
Investigation #8: Object creation overhead
- Creating many small objects can impact performance
- Garbage collector has to work harder to clean up
Understanding JavaScript's memory model isn't just academic—it impacts how we write code daily. By grasping the difference between primitives and references, you can:
- Avoid unintended side effects when passing objects to functions
- Make informed decisions about copying data
- Prevent memory leaks by understanding object lifecycle
- Write more performant code by being mindful of object creation
Remember: In JavaScript, we don't just work with values—we work with references to values. Keeping this mental model clear will make you a more effective JavaScript developer.