JavaScript Object Reference

Understanding how objects are stored and referenced in memory

🔗 What are Object References?

In JavaScript, objects are stored by reference, not by value. This means when you assign an object to a variable, you're storing a reference (pointer) to the object in memory, not the object itself.


let obj1 = {name: "John"};
let obj2 = obj1; // obj2 references the same object as obj1
obj2.name = "Jane"; // Changes the original object
console.log(obj1.name); // "Jane" - both variables point to same object
                                    

Reference Concepts

📍

Reference vs Value

Objects are referenced, primitives are copied

let a = {x: 1}; // Reference
let b = 5;       // Value
🔄

Shared References

Multiple variables can reference same object

let obj1 = {name: "John"};
let obj2 = obj1; // Same reference
⚖️

Equality Comparison

Objects compared by reference, not content

{a: 1} === {a: 1} // false
obj1 === obj2      // true if same reference
📋

Passing to Functions

Objects passed by reference to functions

function modify(obj) {
  obj.changed = true; // Modifies original
}

🔹 Reference vs Value Behavior

Understanding the difference between reference and value types:

// Primitive values are copied
let a = 5;
let b = a;  // b gets a copy of a's value
a = 10;     // Changing a doesn't affect b
console.log("a:", a); // 10
console.log("b:", b); // 5 (unchanged)

// Objects are referenced
let person1 = {name: "Alice", age: 25};
let person2 = person1;  // person2 references the same object
person1.age = 26;       // Modify through person1
console.log("person1 age:", person1.age); // 26
console.log("person2 age:", person2.age); // 26 (same object!)

// Arrays are also objects (referenced)
let arr1 = [1, 2, 3];
let arr2 = arr1;  // arr2 references the same array
arr1.push(4);     // Modify through arr1
console.log("arr1:", arr1); // [1, 2, 3, 4]
console.log("arr2:", arr2); // [1, 2, 3, 4] (same array!)

// Creating a new object breaks the reference
person2 = {name: "Bob", age: 30}; // person2 now references a different object
person1.name = "Alice Updated";
console.log("person1 name:", person1.name); // "Alice Updated"
console.log("person2 name:", person2.name); // "Bob" (different object)

Output:

a: 10
b: 5
person1 age: 26
person2 age: 26
arr1: [1, 2, 3, 4]
arr2: [1, 2, 3, 4]
person1 name: Alice Updated
person2 name: Bob

🔹 Object Equality and Comparison

Objects are compared by reference, not by their content:

// Same content, different objects
let obj1 = {name: "John", age: 30};
let obj2 = {name: "John", age: 30};

console.log("obj1 === obj2:", obj1 === obj2); // false (different references)
console.log("obj1 == obj2:", obj1 == obj2);   // false (different references)

// Same reference
let obj3 = obj1;
console.log("obj1 === obj3:", obj1 === obj3); // true (same reference)

// Comparing object contents manually
function objectsEqual(a, b) {
    let keysA = Object.keys(a);
    let keysB = Object.keys(b);
    
    if (keysA.length !== keysB.length) {
        return false;
    }
    
    for (let key of keysA) {
        if (a[key] !== b[key]) {
            return false;
        }
    }
    return true;
}

console.log("Content equal:", objectsEqual(obj1, obj2)); // true

// Arrays comparison
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
let arr3 = arr1;

console.log("arr1 === arr2:", arr1 === arr2); // false (different arrays)
console.log("arr1 === arr3:", arr1 === arr3); // true (same reference)

// Array content comparison
function arraysEqual(a, b) {
    return a.length === b.length && a.every((val, i) => val === b[i]);
}

console.log("Array content equal:", arraysEqual(arr1, arr2)); // true

// Special cases
let emptyObj1 = {};
let emptyObj2 = {};
console.log("Empty objects equal:", emptyObj1 === emptyObj2); // false

let nullRef1 = null;
let nullRef2 = null;
console.log("null === null:", nullRef1 === nullRef2); // true (null is a value, not reference)

Output:

obj1 === obj2: false
obj1 == obj2: false
obj1 === obj3: true
Content equal: true
arr1 === arr2: false
arr1 === arr3: true
Array content equal: true
Empty objects equal: false
null === null: true

🔹 Functions and Object References

How object references work when passing to functions:

// Objects passed by reference to functions
function modifyObject(obj) {
    obj.modified = true;
    obj.count = (obj.count || 0) + 1;
    console.log("Inside function:", obj);
}

function reassignObject(obj) {
    obj = {newProperty: "I'm a new object"};
    console.log("Reassigned inside function:", obj);
}

let myObject = {name: "Original", value: 42};

console.log("Before modifyObject:", myObject);
modifyObject(myObject);
console.log("After modifyObject:", myObject); // Object was modified

console.log("Before reassignObject:", myObject);
reassignObject(myObject);
console.log("After reassignObject:", myObject); // Object unchanged (reassignment doesn't affect original reference)

// Array example
function addToArray(arr) {
    arr.push("new item");
    return arr;
}

function replaceArray(arr) {
    arr = ["completely", "new", "array"];
    return arr;
}

let myArray = ["original", "items"];

console.log("Original array:", myArray);
addToArray(myArray);
console.log("After addToArray:", myArray); // Array was modified

let newArray = replaceArray(myArray);
console.log("After replaceArray - original:", myArray);  // Unchanged
console.log("After replaceArray - returned:", newArray); // New array

// Practical example: Configuration updater
function updateConfig(config, updates) {
    // This modifies the original config object
    Object.assign(config, updates);
    return config;
}

function createNewConfig(config, updates) {
    // This creates a new object, leaving original unchanged
    return {...config, ...updates};
}

let appConfig = {theme: "light", lang: "en"};

console.log("Original config:", appConfig);
updateConfig(appConfig, {theme: "dark", debug: true});
console.log("After updateConfig:", appConfig); // Modified

let newConfig = createNewConfig(appConfig, {lang: "es"});
console.log("Original after createNewConfig:", appConfig); // Unchanged
console.log("New config:", newConfig); // New object

Output:

Before modifyObject: {name: "Original", value: 42}
Inside function: {name: "Original", value: 42, modified: true, count: 1}
After modifyObject: {name: "Original", value: 42, modified: true, count: 1}
Before reassignObject: {name: "Original", value: 42, modified: true, count: 1}
Reassigned inside function: {newProperty: "I'm a new object"}
After reassignObject: {name: "Original", value: 42, modified: true, count: 1}
Original array: ["original", "items"]
After addToArray: ["original", "items", "new item"]
After replaceArray - original: ["original", "items", "new item"]
After replaceArray - returned: ["completely", "new", "array"]
Original config: {theme: "light", lang: "en"}
After updateConfig: {theme: "dark", lang: "en", debug: true}
Original after createNewConfig: {theme: "dark", lang: "en", debug: true}
New config: {theme: "dark", lang: "es", debug: true}

🔹 Memory Management and Garbage Collection

Understanding how object references affect memory:

// Creating objects and references
let user = {name: "Alice", email: "[email protected]"};
let userRef1 = user;  // Another reference to the same object
let userRef2 = user;  // Yet another reference

console.log("All references point to same object:");
console.log("user === userRef1:", user === userRef1);     // true
console.log("userRef1 === userRef2:", userRef1 === userRef2); // true

// Object stays in memory as long as there's at least one reference
user = null;     // Remove first reference
console.log("After user = null:");
console.log("userRef1 still works:", userRef1.name); // "Alice" (object still exists)

userRef1 = null; // Remove second reference
console.log("userRef2 still works:", userRef2.name); // "Alice" (object still exists)

userRef2 = null; // Remove last reference
// Now the object can be garbage collected (no more references)

// Circular references (can cause memory leaks in older environments)
function createCircularReference() {
    let obj1 = {name: "Object 1"};
    let obj2 = {name: "Object 2"};
    
    obj1.ref = obj2;  // obj1 references obj2
    obj2.ref = obj1;  // obj2 references obj1 (circular!)
    
    return {obj1, obj2};
}

let {obj1, obj2} = createCircularReference();
console.log("Circular reference created:");
console.log("obj1.ref.name:", obj1.ref.name); // "Object 2"
console.log("obj2.ref.name:", obj2.ref.name); // "Object 1"

// To break circular references
obj1.ref = null;
obj2.ref = null;
obj1 = null;
obj2 = null;
// Now both objects can be garbage collected

// WeakMap and WeakSet for weak references
let weakMap = new WeakMap();
let keyObject = {id: 1};

weakMap.set(keyObject, "some value");
console.log("WeakMap has key:", weakMap.has(keyObject)); // true

// If keyObject is garbage collected, the WeakMap entry is automatically removed
keyObject = null; // The WeakMap entry will be cleaned up automatically

// Practical memory management
function createUserManager() {
    let users = new Map(); // Strong references
    let userSessions = new WeakMap(); // Weak references
    
    return {
        addUser(user) {
            users.set(user.id, user);
        },
        
        addSession(user, sessionData) {
            userSessions.set(user, sessionData);
        },
        
        removeUser(userId) {
            let user = users.get(userId);
            users.delete(userId);
            // Session data will be automatically cleaned up
            // when user object is garbage collected
        },
        
        getUserCount() {
            return users.size;
        }
    };
}

let userManager = createUserManager();
let testUser = {id: 1, name: "Test User"};

userManager.addUser(testUser);
userManager.addSession(testUser, {loginTime: Date.now()});

console.log("User count:", userManager.getUserCount()); // 1

userManager.removeUser(1);
testUser = null; // Now session data can be garbage collected

console.log("User count after removal:", userManager.getUserCount()); // 0

Output:

All references point to same object:
user === userRef1: true
userRef1 === userRef2: true
After user = null:
userRef1 still works: Alice
userRef2 still works: Alice
Circular reference created:
obj1.ref.name: Object 2
obj2.ref.name: Object 1
WeakMap has key: true
User count: 1
User count after removal: 0

🔹 Common Reference Pitfalls and Solutions

Avoiding common mistakes with object references:

// Pitfall 1: Unintended object sharing
let defaultSettings = {theme: "light", notifications: true};

function createUser(name) {
    return {
        name: name,
        settings: defaultSettings  // PROBLEM: All users share the same settings object!
    };
}

let user1 = createUser("Alice");
let user2 = createUser("Bob");

user1.settings.theme = "dark";
console.log("user2 theme:", user2.settings.theme); // "dark" - Oops! Both users affected

// Solution: Create a copy of the settings
function createUserCorrect(name) {
    return {
        name: name,
        settings: {...defaultSettings}  // Create a copy
    };
}

let user3 = createUserCorrect("Charlie");
let user4 = createUserCorrect("Diana");

user3.settings.theme = "dark";
console.log("user4 theme:", user4.settings.theme); // "light" - Correct!

// Pitfall 2: Modifying arrays in loops
let items = [{id: 1, active: false}, {id: 2, active: false}];
let activeItems = [];

// PROBLEM: Sharing references
for (let item of items) {
    if (item.id === 1) {
        item.active = true;  // Modifies original array
        activeItems.push(item);
    }
}

console.log("Original items after loop:", items[0].active); // true - Original was modified!

// Solution: Create copies when needed
let itemsCopy = items.map(item => ({...item})); // Deep copy for simple objects
let activeItemsCorrect = [];

for (let item of itemsCopy) {
    if (item.id === 2) {
        item.active = true;  // Modifies copy, not original
        activeItemsCorrect.push(item);
    }
}

console.log("Original items after correct loop:", items[1].active); // false - Original unchanged

// Pitfall 3: Async operations with changing references
let currentUser = {name: "Alice", id: 1};

function simulateAsyncOperation(user) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(`Operation completed for ${user.name}`);
        }, 100);
    });
}

// PROBLEM: Reference might change during async operation
simulateAsyncOperation(currentUser).then(result => {
    console.log(result); // Might not match current user anymore
});

currentUser = {name: "Bob", id: 2}; // Changed before async completes

// Solution: Capture values or create snapshots
let userSnapshot = {...currentUser, name: currentUser.name}; // Capture current state
simulateAsyncOperation(userSnapshot).then(result => {
    console.log("Snapshot result:", result); // Uses captured state
});

// Pitfall 4: Event handlers with object references
let button = {
    text: "Click me",
    clickCount: 0,
    
    handleClick() {
        this.clickCount++;
        console.log(`${this.text} clicked ${this.clickCount} times`);
    }
};

// PROBLEM: 'this' context might be lost
let handler = button.handleClick;
// handler(); // Would cause error or unexpected behavior

// Solution: Bind the context
let boundHandler = button.handleClick.bind(button);
boundHandler(); // Works correctly

// Or use arrow functions in object methods (ES6+)
let buttonES6 = {
    text: "ES6 Button",
    clickCount: 0,
    
    handleClick: function() {
        this.clickCount++;
        console.log(`${this.text} clicked ${this.clickCount} times`);
    },
    
    getHandler() {
        return () => this.handleClick(); // Arrow function preserves 'this'
    }
};

let es6Handler = buttonES6.getHandler();
es6Handler(); // Works correctly

Output:

user2 theme: dark
user4 theme: light
Original items after loop: true
Original items after correct loop: false
Operation completed for Alice
Snapshot result: Operation completed for Bob
Click me clicked 1 times
ES6 Button clicked 1 times

🧠 Test Your Knowledge

What happens when you assign one object to another variable?