Introduction to Parameter Passing in JavaScript
Understanding how JavaScript passes arguments to functions is one of the most fundamental concepts in the language. It directly impacts how data is mutated, how bugs are introduced during state manipulation, and how memory is managed.
The short, definitive answer is: JavaScript is always pass-by-value.
However, this answer frequently confuses developers because variables that hold objects and arrays appear to behave as if they are passed by reference. To master this concept, we need to separate how primitive values are handled from how reference values (objects) are handled under the hood.
1. The Core Rule: JavaScript is Always Pass-by-Value
When you pass a variable into a function, JavaScript takes a copy of that variable’s value and assigns that copy to the function’s parameter.
-
For primitives (numbers, strings, booleans, null, undefined, symbols, bigints), the value stored in the variable is the actual data itself.
-
For objects (including arrays, functions, dates, and custom classes), the value stored in the variable is a reference (or memory address) pointing to where the object lives in memory.
Because JavaScript copies this value, you are always copying something—either the primitive value itself or the reference address pointing to an object. Therefore, the mechanism is strictly pass-by-value.
2. Pass-by-Value with Primitives
When working with primitive data types, the variable holds the actual value. Passing it to a function creates an entirely independent copy. Reassigning or modifying the parameter inside the function has zero effect on the original variable outside the function.
Example: Primitives
function changeValue(num, text, flag) {
num = 100;
text = "Modified";
flag = false;
console.log("Inside function:", num, text, flag);
// Output: 100, "Modified", false
}
let originalNum = 10;
let originalText = "Original";
let originalFlag = true;
changeValue(originalNum, originalText, originalFlag);
console.log("Outside function:", originalNum, originalText, originalFlag);
// Output: 10, "Original", true (Unchanged!)
Why this happens: originalNum holds 10. When passed to changeValue, the engine creates a separate memory slot for the parameter num and copies 10 into it. Changing num inside the function only alters that local copy.
3. The “Pass-by-Reference” Illusion with Objects
When working with objects, developers often assume JavaScript switches to pass-by-reference because mutating an object’s properties inside a function changes the original object globally.
Example: Mutating Object Properties
function updateUserName(userObj) {
userObj.name = "Alice Smith"; // Mutating a property
console.log("Inside function:", userObj.name);
// Output: "Alice Smith"
}
let person = { name: "John Doe", age: 30 };
updateUserName(person);
console.log("Outside function:", person.name);
// Output: "Alice Smith" (The original object was changed!)
Why this happens (Pass-by-Value of the Reference):
-
The variable
persondoes not contain the object itself; it contains a reference (an address pointing to the object in heap memory). -
When
personis passed intoupdateUserName, JavaScript copies that reference value into the parameteruserObj. -
Now, both
personanduserObjpoint to the exact same location in memory. -
When you modify
userObj.name, you are dereferencing that address and changing the properties of the object stored at that memory location. Naturally, looking at the same object through thepersonvariable reveals the updated property.
4. Proving JavaScript is Pass-by-Value (Reassignment Test)
If JavaScript were truly pass-by-reference, reassigning the parameter variable itself to a brand-new object inside the function would overwrite the original variable outside the function. Let’s test what actually happens:
function reassignObject(obj) {
// Reassigning the parameter to a completely new object
obj = { name: "Bob", age: 40 };
console.log("Inside function:", obj);
// Output: { name: "Bob", age: 40 }
}
let currentPerson = { name: "Charlie", age: 25 };
reassignObject(currentPerson);
console.log("Outside function:", currentPerson);
// Output: { name: "Charlie", age: 25 } (Unchanged!)
Breakdown of the Test:
-
currentPersonholds a reference address (e.g.,Ref#101). -
That reference is copied into
obj. Inside the function,objalso equalsRef#101. -
When we write
obj = { name: "Bob", age: 40 }, we allocate a new object in memory (e.g.,Ref#202) and point the localobjvariable to it. -
The outer variable
currentPersonstill points toRef#101. BecausecurrentPersonwas never touched or modified, it remains completely unaffected. This proves beyond doubt that the reference itself was passed by value (as a copy).
5. 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!)
