Understanding Assignment by Reference and by Value in JavaScript​: A Journey of Discovery

Understanding Assignment by Reference and by Value in JavaScript: A Journey of Discovery

While revising data structures in JavaScript, I encountered a challenge that had me scratching my head for a couple of hours. It involved seemingly simple operations, yet my code wasn’t behaving as expected. After diving into the nuances of JavaScript, I realized that the root of my problem lay in understanding how assignment works—whether by reference or by value. In this article, I’ll share what I learned, hoping it will help others facing similar challenges.


Assigning by Reference vs. Assigning by Value

In JavaScript, data types dictate whether variables are assigned by reference or by value.

  • Primitive types (e.g., number, string, boolean, null, undefined, symbol, bigint) are assigned by value.
  • Non-primitive types (e.g., object, array, function) are assigned by reference.


What Does This Mean?

When assigning a primitive value:

  • A copy of the value is created.
  • Modifying one variable does not affect the other.

Example:

let a = 42;
let b = a;  // Copy the value of `a` into `b`
b = 100;
console.log(a);  // Output: 42
console.log(b);  // Output: 100
        

When assigning a non-primitive value:

  • A reference to the same memory location is shared.
  • Modifying the value through one variable affects the other.

Example:

const objA = { name: "Pablo" };
const objB = objA;  // Both point to the same object in memory
objB.name = "Thobias";
console.log(objA.name); // Output: "Thobias"
        

Shallow Copy vs. Deep Copy


Shallow Copy

A shallow copy creates a new object with the top-level properties copied. If these properties are references (e.g., objects or arrays), they still point to the original memory location.

Example:

const original = { name: "Pablo", address: { city: "Florianópolis" } };
const shallowCopy = { ...original };

shallowCopy.name = "Thobias";
shallowCopy.address.city = "São Paulo";

console.log(original.name); // Output: "Pablo"
console.log(original.address.city); // Output: "São Paulo"        

In the example above, while the name property is independent in the shallow copy, the address object is still shared.


Deep Copy

A deep copy creates a completely independent copy of the object, including nested objects or arrays.

Example using structuredClone:

const original = { name: "Pablo", address: { city: "Florianópolis" } };
const deepCopy = structuredClone(original);

deepCopy.address.city = "São Paulo";

console.log(original.address.city); // Output: "Florianópolis"
console.log(deepCopy.address.city); // Output: "São Paulo"
        

Other ways to create a deep copy:

  • Using JSON.parse(JSON.stringify(object)) (limitations with functions, undefined, or circular references).
  • Libraries like lodash (_.cloneDeep(value)).


How Do structuredClone and the Spread Operator Work Under the Hood?


The structuredClone Method

The structuredClone method is a built-in JavaScript utility for creating deep copies of objects. It serializes the object, including its nested structures, and then deserializes it back into memory as a new, independent object.


Key Characteristics:

  • Supports most JavaScript types, including arrays, objects, dates, and typed arrays.
  • Handles circular references, which makes it robust compared to JSON-based cloning.

Limitations:

  • Does not copy functions or prototype methods.


How structuredClone Is actually implemented

Recursive Cloning: The API traverses the object tree recursively, visiting every property and node.

Custom Handlers for Built-in Types: Special handling exists for complex objects like Date, Map, Set, ArrayBuffer, and typed arrays. For example:

• Map objects are cloned by iterating over their keys and values.

• Set objects are cloned by iterating over their entries.

Cycle Detection: To prevent infinite loops in circular references, structuredClone maintains an internal map of already cloned objects during the traversal.

Efficient Memory Management: Transferable objects (like ArrayBuffer) can be “moved” instead of copied, making cloning faster for such objects.


Example:

function structuredClone(obj, seen = new WeakMap()) {
    // Handle primitive values
    if (obj === null || typeof obj !== 'object') {
        return obj; // Primitive types are returned as-is
    }

    // Handle if an attribute has already been copied
    if (seen.has(obj)) {
        return seen.get(obj);
    }

    // Handle special object types
    if (obj instanceof Date) {
        return new Date(obj.getTime());
    }
    if (obj instanceof Map) {
        const result = new Map();
        seen.set(obj, result); // Record the reference to handle cycles
        for (const [key, value] of obj.entries()) {
            result.set(structuredClone(key, seen), structuredClone(value, seen));
        }
        return result;
    }
    if (obj instanceof Set) {
        const result = new Set();
        seen.set(obj, result); // Record the reference to handle cycles
        for (const value of obj.values()) {
            result.add(structuredClone(value, seen));
        }
        return result;
    }
    if (obj instanceof Array) {
        const result = [];
        seen.set(obj, result); // Record the reference to handle cycles
        for (const value of obj) {
            result.push(structuredClone(value, seen));
        }
        return result;
    }
    if (obj instanceof ArrayBuffer) {
        return obj.slice(0); // Clone the buffer
    }
    if (ArrayBuffer.isView(obj)) {
        // Handle typed arrays (e.g., Uint8Array)
        return new obj.constructor(obj.buffer.slice(0), obj.byteOffset, obj.length);
    }

    // Clone plain objects
    const result = {};
    seen.set(obj, result); // Record the reference to handle cycles
    for (const [key, value] of Object.entries(obj)) {
        result[key] = structuredClone(value, seen);
    }
    return result;
}

// Example Usage
const original = {
    name: "Pablo",
    details: {
        age: 29,
        hobbies: ["coding", "reading"],
    },
    nested: new Map([["key", "value"]]),
    buffer: new ArrayBuffer(8),
};

const clone = structuredClone(original);
console.log(clone); // Deeply cloned object
console.log(clone === original); // false
console.log(clone.details === original.details); // false        

The Spread Operator (...)

The spread operator is a shallow copy mechanism. When used with objects or arrays, it copies the top-level properties but retains references for nested structures.

How It Works:

  1. Allocates a new memory space for the outer object.
  2. Iterates through the enumerable properties of the source object, copying them to the new object.
  3. For nested objects or arrays, the references are retained rather than creating new instances.

Example:

const source = { name: "Pablo", nested: { key: "value" } };
const shallowCopy = { ...source };

shallowCopy.nested.key = "changed";
console.log(source.nested.key); // Output: "changed"        

Limitations:

  • It cannot handle deep cloning.
  • Circular references or complex types (e.g., Maps, Sets) are not deeply copied.


Here’s a quick breakdown:

1. Shallow Copy for Objects:

• Copies the top-level properties, but any nested objects or arrays are still references to the original.

2. Shallow Copy for Arrays:

• Copies elements into a new array, but nested arrays or objects remain references.


function shallowClone(obj) {
    // Only handle objects
    if (obj === null || typeof obj !== "object") {
        return obj;
    }

    // Initialize a new object or array
    const result = Array.isArray(obj) ? [] : {};

    // Copy all enumerable properties
    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            result[key] = obj[key];
        }
    }

    return result;
}

// Example Usage
const original = {
    name: "Pablo",
    details: {
        age: 29,
        hobbies: ["coding", "reading"],
    },
};

const clone = shallowClone(original);
console.log(clone); // Shallow copy of the original object
console.log(clone === original); // false
console.log(clone.details === original.details); // true (nested objects are still references)        

Practical Takeaways

When to Use Shallow Copy

  • If you only need to work with the top-level properties.
  • When the structure is flat and nested references are irrelevant.

When to Use Deep Copy

  • If the object contains nested structures that you need to modify independently.
  • To avoid unintentional side effects caused by shared references.


Conclusion

Understanding assignment by reference and by value is crucial for writing predictable and bug-free JavaScript code. Knowing when to use shallow versus deep copies can save you from unexpected behaviors and debugging nightmares. This learning process has not only enhanced my technical skills but also deepened my appreciation for JavaScript's behavior and nuances.

I hope this article clarifies the confusion for anyone grappling with similar issues. Feel free to share your experiences or questions in the comments—I’d love to hear from you!


References



To view or add a comment, sign in

More articles by Pablo Thobias Braz Carminatti

Insights from the community

Others also viewed

Explore topics