Key Takeaway: JS/TS is ALWAYS Pass-by-Value. The "value" being passed is either a primitive directly, or a reference (a pointer) to an object.
- Types:
number,string,boolean,bigint,symbol,null,undefined - What's Passed: An exact copy of the value itself.
- Behavior:
- When you pass a primitive to a function, the function gets its own, independent copy.
- Re-assigning the parameter inside the function does not affect the original variable outside.
- Rust Analogy: Very similar to passing a
Copytype (e.g.,u32,bool) by value in Rust.
Example:
function modifyPrimitive(x: number) {
x = 42; // This 'x' is a local copy.
console.log(`Inside: x = ${x}`); // 42
}
let num = 10;
console.log(`Before: num = ${num}`); // 10
modifyPrimitive(num);
console.log(`After: num = ${num}`); // 10 (Original 'num' is unchanged)- Types:
object,array,function,Date,Map,Set,classinstances,RegExp,Promise,Error, etc. - What's Passed: A copy of the reference (a pointer/memory address) to the object on the heap.
- Behavior:
- Both the original variable and the function's parameter now point to the same object in memory.
- Mutating properties of the object through the function parameter IS visible to the original variable. (Think
&mut Tin Rust, where you can modify the data through the reference). - Re-assigning the parameter variable itself to a new object is NOT visible to the original variable. This just makes the local parameter point to a different object, leaving the original variable untouched. (Think
let mut x = ...; x = new_value;for a local variable, it doesn't affect an outside binding).
- Rust Analogy:
- Passing an object reference is like passing
&T(for reading) or&mut T(for modifying properties) in Rust. You're working with the same underlying data. - There's no explicit ownership transfer like in Rust; multiple references can point to the same object simultaneously. Memory management (garbage collection) handles deallocation when no references remain.
- Passing an object reference is like passing
Examples:
// --- Case A: Mutating the object's properties (Visible outside) ---
function modifyArrayInPlace(arr: number[]) {
arr.push(99); // Mutates the *original* array pointed to by 'arr'
arr[0] = 100; // Also visible outside
console.log(`Inside (mutation): arr =`, arr); // [100, 2, 99]
}
const myArray = [1, 2];
console.log(`Before (mutation): myArray =`, myArray); // [1, 2]
modifyArrayInPlace(myArray);
console.log(`After (mutation): myArray =`, myArray); // [100, 2, 99] (Changed!)
// --- Case B: Re-assigning the parameter itself (NOT visible outside) ---
function reassignParameter(obj: { key: string }) {
obj = { key: "new object" }; // 'obj' now points to a *new*, different object.
console.log(`Inside (re-assignment): obj =`, obj); // { key: 'new object' }
}
let myObject = { key: "original object" };
console.log(`Before (re-assignment): myObject =`, myObject); // { key: 'original object' }
reassignParameter(myObject);
console.log(`After (re-assignment): myObject =`, myObject); // { key: 'original object' } (Unchanged!)- No Implicit Deep Cloning: Unlike Rust's
Clonetrait which can be implemented for deep copies, JS/TS functions generally create shallow copies by default. - Shallow Copy: Copies the top-level structure, but nested objects are still references to the original nested objects.
[...originalArray]{ ...originalObject }Object.assign({}, originalObject)
- Deep Copy:
structuredClone(obj): The modern, preferred built-in way (available in most modern environments). Handles many complex types but not functions or DOM nodes.JSON.parse(JSON.stringify(obj)): A hacky method, with significant limitations (loses functions,undefined, turnsDateobjects into strings, etc.).- Third-party libraries (e.g., Lodash's
_.cloneDeep): Offer more robust deep cloning solutions.