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.
What Does This Mean?
When assigning a primitive value:
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:
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:
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:
Limitations:
Recommended by LinkedIn
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.
Example:
const source = { name: "Pablo", nested: { key: "value" } };
const shallowCopy = { ...source };
shallowCopy.nested.key = "changed";
console.log(source.nested.key); // Output: "changed"
Limitations:
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
When to Use Deep Copy
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