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");

Apr 3, 2025 - 04:28
 0
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");

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:

  1. Avoid unintended side effects when passing objects to functions
  2. Make informed decisions about copying data
  3. Prevent memory leaks by understanding object lifecycle
  4. 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.