JavaScript: Is it Pass-by-Value or Pass-by-Reference?
One of the most common questions in JavaScript interviews is: "How are variables passed in JS?". The answer often leads to confusion. Let's break it down simply and accurately.
The Golden Rule: Everything is Pass-by-Value
In JavaScript, everything is passed by value. However, the "value" depends on what type of data you are dealing with.
1. Primitives (Number, String, Boolean, Symbol, BigInt, null, undefined)
When you pass a primitive, JS makes a complete copy of the data. The original and the copy are totally independent.
let a = 10;
function change(x) {
x = 20;
}
change(a);
console.log(a); // Result: 10 (Original stays the same)
Why? Primitives are stored directly in the variable. When you pass them, JavaScript copies the actual value.
let name = "Alice";
let copyName = name;
copyName = "Bob";
console.log(name); // "Alice" - unchanged
console.log(copyName); // "Bob"
2. Objects and Arrays (The "Copy of a Reference")
Objects are also passed by value, but the value being copied is the reference (address) to where the object lives in memory. This is often called "Pass-by-sharing".
- You aren't copying the whole object.
- You are copying the "remote control" to that object.
let user = { name: "Neo" };
function rename(obj) {
obj.name = "Smith"; // Both variables point to the same object
}
rename(user);
console.log(user.name); // Result: "Smith"
What Happens in Memory?
let obj1 = { value: 42 };
let obj2 = obj1; // Copying the reference, not the object
obj2.value = 100;
console.log(obj1.value); // 100 - They both point to the same object!
console.log(obj1 === obj2); // true - Same reference
Reassignment vs Mutation
Important distinction: Reassigning the variable does NOT affect the original, but mutating the object does.
let original = { count: 5 };
function reassign(obj) {
obj = { count: 10 }; // Creates new object, breaks the link
}
function mutate(obj) {
obj.count = 10; // Modifies the existing object
}
reassign(original);
console.log(original.count); // 5 - Reassignment didn't affect original
mutate(original);
console.log(original.count); // 10 - Mutation changed the original
Shallow Copy vs. Deep Copy
Because objects are shared, modifying a copy often accidentally modifies the original. This is where cloning comes in.
Shallow Copy
Copies only the first level. Nested objects are still shared.
How: { ...original } or Object.assign({}, original) or [...array]
Danger: Changing copy.address.city will change original.address.city.
const user = {
name: "Alice",
address: {
city: "NYC"
}
};
const shallowCopy = { ...user };
shallowCopy.name = "Bob"; // Safe: Creates new string
shallowCopy.address.city = "LA"; // Danger: Modifies shared object
console.log(user.name); // "Alice" - unchanged
console.log(user.address.city); // "LA" - changed! ⚠️
Array example:
const nested = [[1, 2], [3, 4]];
const copy = [...nested];
copy[0][0] = 99; // Modifies the original!
console.log(nested[0][0]); // 99 ⚠️
Deep Copy
Creates a completely independent clone of the entire tree.
Modern way: structuredClone(original) ✅
const user = {
name: "Alice",
address: { city: "NYC" },
hobbies: ["reading"]
};
const deepCopy = structuredClone(user);
deepCopy.address.city = "LA";
deepCopy.hobbies.push("coding");
console.log(user.address.city); // "NYC" - unchanged ✅
console.log(user.hobbies); // ["reading"] - unchanged ✅
Legacy way: JSON.parse(JSON.stringify(original))
Limitations:
- Loses functions
- Loses
undefinedvalues - Loses
Symbolkeys - Can't handle circular references
- Loses
Dateobjects (converts to string)
const obj = {
date: new Date(),
fn: () => console.log("hi"),
undef: undefined
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.date); // String, not Date object
console.log(copy.fn); // undefined - lost!
console.log(copy.undef); // undefined - lost!
The Power of Immutability
In modern development (especially with React), we follow the principle of Immutability: Never change the original data. Instead, always return a new version.
Why?
- Predictability: You always know what your data looks like.
-
Performance: Frameworks can check if data changed by simply comparing references (
oldState === newState) instead of checking every property. - Time-travel debugging: Tools like Redux DevTools can track state changes.
- Avoid bugs: No unexpected side effects from distant parts of your code.
How to Stay Immutable
Arrays
Mutation (Bad):
const todos = ["task1", "task2"];
todos.push("task3"); // Mutates original
Immutable (Good):
const todos = ["task1", "task2"];
const newTodos = [...todos, "task3"]; // Creates new array
Common immutable array operations:
const arr = [1, 2, 3, 4, 5];
// Add item
const added = [...arr, 6];
// Remove item by index
const removed = arr.filter((_, i) => i !== 2); // Removes index 2
// Update item by index
const updated = arr.map((item, i) => i === 2 ? 99 : item);
// Insert at specific position
const inserted = [...arr.slice(0, 2), 99, ...arr.slice(2)];
Objects
Mutation (Bad):
const user = { name: "Alice", age: 25 };
user.age = 26; // Mutates original
Immutable (Good):
const user = { name: "Alice", age: 25 };
const updatedUser = { ...user, age: 26 }; // Creates new object
Nested updates:
const state = {
user: {
name: "Alice",
address: {
city: "NYC"
}
}
};
// Update nested property immutably
const newState = {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: "LA"
}
}
};
React-Specific Patterns
React relies heavily on immutability for efficient rendering.
State Updates
Wrong way:
const [user, setUser] = useState({ name: "Alice", age: 25 });
// This won't trigger re-render!
const updateAge = () => {
user.age = 26;
setUser(user); // Same reference, React ignores it
};
Correct way:
const [user, setUser] = useState({ name: "Alice", age: 25 });
const updateAge = () => {
setUser({ ...user, age: 26 }); // New reference, React re-renders
};
Array State Updates
const [items, setItems] = useState([1, 2, 3]);
// Add item
setItems([...items, 4]);
// Remove item
setItems(items.filter(item => item !== 2));
// Update item
setItems(items.map(item => item === 2 ? 99 : item));
Why React Cares About References
// React's simplified internal check
if (prevState === newState) {
// Skip rendering, nothing changed
} else {
// Re-render component
}
This is why mutating state directly breaks React:
const [todos, setTodos] = useState([]);
// Wrong
const addTodo = (text) => {
todos.push(text);
setTodos(todos); // Same reference! React won't re-render
};
// Correct
const addTodo = (text) => {
setTodos([...todos, text]); // New reference, React re-renders
};
Common Pitfalls and Solutions
Pitfall 1: Accidental Mutation in Callbacks
const handleClick = () => {
const items = [...currentItems];
items.sort(); // sort() mutates the array!
setItems(items);
};
// Better
const handleClick = () => {
const items = [...currentItems];
const sorted = items.sort(); // Still mutates, but we have a copy
setItems(sorted);
};
// Best
const handleClick = () => {
const sorted = [...currentItems].sort();
setItems(sorted);
};
Pitfall 2: Forgetting About Nested Objects
// Shallow copy doesn't protect nested data
const newState = { ...state };
newState.user.name = "Bob"; // Mutates original state.user!
// Deep copy or nested spread
const newState = {
...state,
user: {
...state.user,
name: "Bob"
}
};
Pitfall 3: Reference Equality in Dependencies
// Creates new object on every render
const MyComponent = () => {
const config = { theme: "dark" }; // New reference each render
useEffect(() => {
applyConfig(config);
}, [config]); // Runs on every render!
};
// Stable reference
const MyComponent = () => {
const config = useMemo(() => ({ theme: "dark" }), []);
useEffect(() => {
applyConfig(config);
}, [config]); // Only runs when needed
};
Performance Considerations
When to Use Each Approach
| Scenario | Recommended Approach |
|---|---|
| Small objects | Shallow copy { ...obj }
|
| Nested structures |
structuredClone() or nested spreads |
| Large objects updated frequently | Consider Immer library |
| Arrays with primitives | Spread [...arr]
|
| Arrays with objects |
structuredClone() or map with spreads |
Using Libraries for Complex Immutability
For deeply nested state, consider Immer:
import produce from 'immer';
const newState = produce(state, draft => {
// Mutate draft freely, Immer makes it immutable
draft.user.address.city = "LA";
draft.todos.push("New task");
});
Summary Table
| Data Type | Passed by... | Result of Modification | Copy Method |
|---|---|---|---|
| Primitives | Value | Original is safe | Automatic |
| Objects | Value (Reference) | Original property changes |
{ ...obj } (shallow)structuredClone(obj) (deep) |
| Arrays | Value (Reference) | Original item changes |
[...arr] (shallow)structuredClone(arr) (deep) |
| Cloned Object | New Reference | Original is safe | Depends on copy type |
Quick Reference Cheat Sheet
// ========== PRIMITIVES ==========
let a = 5;
let b = a; // Copy
b = 10;
// a is still 5
// ========== OBJECTS ==========
let obj1 = { x: 1 };
let obj2 = obj1; // Reference copy
obj2.x = 2;
// obj1.x is now 2!
// Shallow copy
let obj3 = { ...obj1 };
// Deep copy
let obj4 = structuredClone(obj1);
// ========== ARRAYS ==========
let arr1 = [1, 2, 3];
let arr2 = arr1; // Reference copy
arr2.push(4);
// arr1 is now [1, 2, 3, 4]!
// Shallow copy
let arr3 = [...arr1];
// Deep copy
let arr4 = structuredClone(arr1);
// ========== IMMUTABLE UPDATES ==========
// Object
const newObj = { ...oldObj, prop: newValue };
// Array - add
const newArr = [...oldArr, newItem];
// Array - remove
const filtered = oldArr.filter(item => item.id !== idToRemove);
// Array - update
const updated = oldArr.map(item =>
item.id === targetId ? { ...item, prop: newValue } : item
);
Conclusion
Understanding that JavaScript passes a copy of the reference for objects is a "lightbulb moment" for many developers. By mastering Shallow vs. Deep copying and embracing Immutability, you will write cleaner, bug-free code—especially in modern frameworks like React.
Key Takeaways:
- JS is always pass-by-value (but the value might be a reference)
- Primitives are safe from external changes
- Objects and arrays share references—be careful!
- Use spread or
structuredClone()to create copies - Embrace immutability for predictable, performant code
- React relies on reference equality for optimization
Now go forth and never mutate state directly again! 🚀
Top comments (0)