In JavaScript, the way data is passed to functions or assigned to variables is often a source of significant confusion. To understand this, you must distinguish between Primitive types and Objects. While it is technically accurate to say that JavaScript is always “Pass-by-Value,” the “value” being passed for an object is actually the reference (memory address) to that object. This behavior is frequently referred to as Pass-by-Sharing.
1. Primitives: Strict Pass-by-Value
Primitive types (String, Number, Boolean, null, undefined, Symbol, and BigInt) are passed by value. When you assign a primitive to a variable or pass it to a function, JavaScript creates a total copy of that data in a new memory slot.
Changes made to the copy do not affect the original variable because they occupy completely different locations on the Stack.
let score = 100;
function changeScore(val) {
val = 200; // This only changes the local copy
}
changeScore(score);
console.log(score); // 100 (The original remains untouched)
2. Objects and Arrays: Pass-by-Reference (Value of Reference)
Objects, Arrays, and Functions are complex data structures stored in the Heap. The variable on the Stack does not contain the object itself; instead, it contains a memory address (pointer) that points to the object’s location in the Heap.
When you pass an object to a function, you are passing a copy of that reference. Both the original variable and the function parameter now point to the exact same object in the Heap.
The Behavior: Mutation
Since both variables point to the same memory address, modifying a property of the object inside the function will affect the original object outside the function.
let user = { name: "Julian" };
function mutateUser(obj) {
obj.name = "Skyler"; // Modifying a property through the reference
}
mutateUser(user);
console.log(user.name); // "Skyler" (Original object was changed)
The Exception: Reassignment
A critical distinction occurs if you try to reassign the entire object inside a function. Reassigning the parameter creates a new reference on the stack, breaking the link to the original object.
let car = { color: "red" };
function replaceCar(obj) {
obj = { color: "blue" }; // Reassigning the pointer to a NEW object in the Heap
}
replaceCar(car);
console.log(car.color); // "red" (The link was broken, original remains)
3. The Reassignment Trap
The most critical distinction in “Pass-by-Sharing” occurs when you try to reassign the entire object inside a function.
If you reassign the parameter to a new object, you are simply breaking the link to the original reference and pointing the local variable to a new memory address. The original object outside the function remains unchanged.
let player = { level: 1 };
function levelUp(obj) {
// Reassigning 'obj' to a brand new object
obj = { level: 99 };
// The local 'obj' now points to a new address; the global 'player' still points to level: 1
}
levelUp(player);
console.log(player.level); // 1
4. Practical Implications: Defensive Coding
Because objects are passed by reference, unexpected side effects are common in large applications. To prevent functions from accidentally modifying your data, you should use Immutability patterns.
A. Shallow Cloning
Use the spread operator (...) or Object.assign() to create a new object before passing it or modifying it. This creates a new memory reference for the top-level properties.
const original = { a: 1, b: 2 };
const copy = { ...original };
copy.a = 99;
console.log(original.a); // 1 (Safe!)
B. The Deep Clone Problem
Note that a shallow clone only goes one level deep. If your object contains nested objects, those nested objects are still passed by reference. For deeply nested structures, you may need a deep clone.
const user = { id: 1, meta: { color: "red" } };
const shallow = { ...user };
shallow.meta.color = "blue";
console.log(user.meta.color); // "blue" (Nested objects were NOT cloned!)
