Question

JavaScript Pass by Value: Objects, References, and Function Arguments

javascriptpass-by-referencepass-by-value

Question

JavaScript primitive values such as numbers and strings are commonly described as being passed by value. However, object arguments can appear to behave differently: a function can mutate an object supplied by the caller, but it cannot replace the caller's variable by assigning a new object to its parameter.

What is the correct way to describe JavaScript's argument-passing convention? Is JavaScript pass-by-value or pass-by-reference, and how does the ECMAScript specification define the relevant semantics?

Short Answer

JavaScript is pass-by-value. When you pass an object to a function, the value being copied is an object reference value. Both the caller's variable and the function parameter can therefore refer to the same object and mutate it, but reassigning the parameter changes only the local copy.

Concept

JavaScript function arguments are passed by value.

A value can be either:

  • A primitive value, such as a number, string, boolean, bigint, symbol, undefined, or null.
  • An object reference value: a value that identifies an object in memory.

For primitives, copying the value is easy to observe because primitives are immutable:

function increase(value) {
  value = value + 1;
}

let score = 10;
increase(score);

console.log(score); // 10

score is unchanged because the function receives its own copy of 10.

For objects, JavaScript also copies a value—but that value refers to an object. After the copy, the caller's variable and the parameter refer to the same object:

function rename(user) {
  user.name = "Ada";
}

const person = { name: "Grace" };
rename(person);

console.log(person.name); // "Ada"

The function did not receive the caller's variable itself. It received a copied reference value that points to the same object, so changing an object property is visible through both references.

The distinction becomes clear when the parameter is reassigned:

function replaceUser(user) {
  user = { name: "Ada" };
}

const person = { name: "Grace" };
replaceUser(person);

console.log(person.name); // "Grace"

user is a local parameter. Assigning a new object to it does not alter person.

This matters because "pass-by-reference" has a stricter meaning in programming-language terminology: the called function receives access to the caller's variable or storage location. If JavaScript used pass-by-reference, assigning to a parameter would replace the caller's variable too. It does not, so JavaScript is not pass-by-reference.

At a specification level, ECMAScript describes evaluating argument expressions into argument values, then invoking the function with a list of those values. Function parameters are initialized as local bindings from those argument values. The specification uses abstract operations such as GetValue while evaluating expressions and Call while invoking callable values; it does not define JavaScript as "pass-by-reference." The observable behavior is value passing, including copied object reference values.

Mental Model

Think of a variable as a label containing a value.

  • For a primitive, the label contains the data itself, such as 42.
  • For an object, the label contains a map location that leads to the object.

When calling a function, JavaScript photocopies the value on the label into a new label—the parameter.

For an object, photocopying the map location means both labels lead to the same house. Either person can repaint the house (mutate its properties). But if the function replaces its own map with directions to a different house (reassigns the parameter), the caller still has the original map.

Take Quiz

Syntax and Examples

A parameter is a local variable initialized with the argument's value.

function inspect(item) {
  // `item` starts with a copy of the argument value.
  console.log(item);
}

inspect("hello");

Primitive argument

function setToZero(number) {
  number = 0;
}

let count = 5;
setToZero(count);

console.log(count); // 5

The parameter number is separate from count.

Object mutation

function markComplete(task) {
  task.completed = true;
}

const task = { title: "Write tests", completed: false };
markComplete(task);

console.log(task.completed); // true

task and task inside the function refer to the same object, so property mutation is shared.

Object reassignment

function resetTask(task) {
  task = { title: "New task", completed: false };
}

const task = { title: "Write tests", completed: true };
resetTask(task);

console.log(task); // { title: "Write tests", completed: true }

The assignment replaces only the function's local parameter value.

Step by Step Execution

Trace this example:

function updateProfile(profile) {
  profile.role = "admin";
  profile = { name: "Lin", role: "guest" };
  profile.role = "editor";
}

const account = { name: "Lin", role: "member" };
updateProfile(account);

console.log(account); // { name: "Lin", role: "admin" }
  1. account contains an object reference value for the object { name: "Lin", role: "member" }.
  2. updateProfile(account) copies that reference value into the local parameter profile.
  3. profile.role = "admin" mutates the original shared object. account can observe this change.
  4. profile = { name: "Lin", role: "guest" } assigns a new object reference to the local profile binding only.
  5. profile.role = "editor" mutates the new local object, not the object held by account.
  6. After the function returns, account still refers to the first object, whose role is now "admin".

Real World Use Cases

  • Updating state objects: A function may add a field to a request context, such as request.user.
  • Modifying arrays: Functions can mutate a passed array with push, sort, or splice.
  • API payload construction: A helper can add headers or metadata to a shared options object.
  • Configuration handling: A function may intentionally clone a configuration object before changing it, preventing unexpected changes for the caller.
  • DOM work: A function can change a passed DOM element's text or classes because the parameter refers to the same element object.

Example: intentionally avoiding mutation in an application setting:

function addItem(cart, item) {
  return [...cart, item];
}

const currentCart = ["book"];
const nextCart = addItem(currentCart, "pen");

console.log(currentCart); // ["book"]
console.log(nextCart);    // ["book", "pen"]

The function returns a new array rather than mutating the caller's array.

Take Quiz

Real Codebase Usage

Developers use this behavior deliberately and protect against accidental mutation.

Validate without changing input

function isValidEmail(email) {
  return typeof email === "string" && email.includes("@");
}

Primitive arguments are naturally safe from mutation because strings are immutable.

Use guard clauses before mutation

function applyDiscount(order, percentage) {
  if (!order || percentage <= 0) {
    return;
  }

  order.total *= 1 - percentage / 100;
}

This intentionally changes the order object only after validation.

Return a copy for predictable state updates

function updateUserName(user, name) {
  return {
    ...user,
    name
  };
}

This pattern is common in UI state management and reducers because it avoids changing the input object.

Clone nested data when necessary

A shallow copy only copies top-level properties:

const original = { settings: { theme: "light" } };
const copy = { ...original };

copy.settings.theme = "dark";
console.log(original.settings.theme); // "dark"

Both objects still refer to the same nested settings object. Use structuredClone when a deep copy is appropriate and supported by the data type:

const independentCopy = structuredClone(original);

Common Mistakes

Calling object behavior pass-by-reference

This description is misleading:

"Objects are passed by reference."

It can suggest that a parameter is an alias for the caller's variable. In JavaScript, it is not. Prefer:

"JavaScript passes values. For objects, the value is a reference to the object."

Expecting parameter reassignment to update the caller

function clearList(list) {
  list = [];
}

const names = ["Ada", "Lin"];
clearList(names);

console.log(names); // ["Ada", "Lin"]

To intentionally empty the existing array, mutate it:

function clearList(list) {
  list.length = 0;
}

Or return a replacement and assign it in the caller:

function emptyList() {
  return [];
}

let names = ["Ada", "Lin"];
names = emptyList();

Assuming const makes an object immutable

const user = { name: "Ada" };
user.name = "Lin"; // Valid

const prevents reassignment of user; it does not prevent object property changes.

Accidentally mutating with array methods

Some methods mutate the array, including push, pop, sort, reverse, and splice.

const numbers = [3, 1, 2];
numbers.sort();
console.log(numbers); // [1, 2, 3]

If mutation is unwanted, copy first:

const sorted = [...numbers].sort();

Confusing equality with object contents

{} === {}; // false

Objects compare by reference identity, not by whether their properties look the same.

Comparisons

IdeaWhat the function receivesCan it mutate the caller's object?Can parameter reassignment replace the caller's variable?
JavaScript primitive argumentA copied primitive valueNot applicableNo
JavaScript object argumentA copied object reference valueYes, through the shared objectNo
True pass-by-referenceAccess to the caller's variable/locationYes, if the value supports mutationYes

Mutation vs reassignment

OperationExampleAffects caller's object?
Property mutationuser.name = "Ada"Yes, when both values refer to the same object
Array mutationitems.push("pen")Yes
Parameter reassignmentuser = { name: "Ada" }No
Returning a replacementreturn { ...user, name: "Ada" }Only if the caller assigns the return value

Shallow copy vs deep copy

TechniqueCopies top-level object?Copies nested objects?
{ ...object }YesNo
Object.assign({}, object)YesNo
structuredClone(object)YesYes, for supported cloneable values
Take Quiz

Cheat Sheet

  • JavaScript passes all function arguments by value.

  • Primitive values are copied directly.

  • An object value is a reference to an object; that reference value is copied.

  • A function can mutate a shared object:

    function f(obj) { obj.x = 1; }
    
  • A function cannot reassign the caller's variable:

    function f(obj) { obj = {}; }
    
  • const prevents variable reassignment, not object mutation.

  • Spread syntax ({ ...obj }, [...array]) makes a shallow copy.

  • Return a new object or array when callers should receive a replacement.

  • Avoid saying JavaScript "passes objects by reference" without clarification; say it passes object reference values by value.

FAQ

Is JavaScript pass-by-value or pass-by-reference?

JavaScript is pass-by-value. Object reference values are copied when passed to functions.

Why can a function change an object passed to it?

The parameter and the caller's variable refer to the same object. Changing a property changes that shared object.

Why does assigning a new object to a parameter not change the original variable?

The parameter is a separate local binding containing a copied value. Reassignment changes only that local binding.

Are arrays passed by reference in JavaScript?

Arrays are objects, so they follow the same rule: JavaScript passes a copied reference value. Array mutations are visible to the caller; parameter reassignment is not.

Are strings passed by reference in JavaScript?

No. Strings are primitive values and are passed by value. Also, strings are immutable.

Does const stop a function from changing an object?

No. const stops rebinding the variable to another value. It does not freeze the object or its properties.

How can I prevent a function from mutating my object?

Use a copying convention, pass a clone, or use Object.freeze when appropriate. For nested mutable data, remember that a shallow copy does not clone nested objects.

Does the ECMAScript specification call this pass-by-value?

The specification defines behavior through argument evaluation, values, and parameter bindings rather than using that informal label. Its defined behavior matches pass-by-value: arguments become values used to initialize local parameter bindings.

Take Quiz

Mini Project

Description

Build a small profile-update utility that demonstrates the difference between mutating a passed object and returning a replacement object. This mirrors a common decision in application code: should a helper update shared data, or should it create the next version of the data?

Goal

Create functions that update a user profile in both mutable and immutable styles, then verify the different results.

Requirements

Define a user object with a name, role, and settings object.

Take Quiz

Keep learning

Related questions

View more