Understanding JavaScript Variable Passing: Pass by Value, Pass by Reference, or Pass by Sharing?
Introduction
As mentioned in Huli's article, "Deep Dive into JavaScript Parameter Passing: Call by Value or Reference?": "Coming back to research parameter passing was a beautiful mistake - I originally intended to write about deep and shallow copying." My experience writing this article was similar.
I initially encountered issues with object shallow copy
and deep copy
at work and planned to research and write about copying. However, as I wrote, I realized I needed to first explain the pass by reference
variable passing mechanism to better clarify why shallow and deep copying are necessary.
So I thought I'd simply start by briefly explaining pass by value
and pass by reference
as an introduction to my article on copying? Not quite, because deeper investigation revealed concepts like pass by sharing
and claims that JavaScript is all pass by value
- knowledge I hadn't previously encountered.
As I continued researching, I decided to write this article - partly because I found the topic fascinating, partly because there was substantial information to cover, and partly because knowledge only truly becomes your own when you organize and output it. And so begins this exploration of pass by value
, pass by reference
, and pass by sharing
in JavaScript.
Primitive Types and Objects in Memory
To properly understand the concepts of pass by value
, pass by reference
, and pass by sharing
, we need to understand several aspects of JavaScript:
- The two data types
- How variable data is stored in memory
- The behavior and results of copying variables
- The behavior and results of passing variables to functions
- With this knowledge, we can understand
pass by value
,pass by reference
, andpass by sharing
!
Starting with the first item, JavaScript has two types of data:
- Primitive types: Representing single values, such as
string
,number
,boolean
,null
,undefined
,symbol
. - Object types: Representing complete concepts or collections of data, which can contain multiple primitive types and have their own properties or methods, such as
object
,array
,function
.
// Primitive type variables
const a = 5;
const b = '15';
const c = true;
const d = null;
const e = undefined;
......
// Object type variables
const objectData = {
a: 'one',
b: 2,
c: true,
};
const arrayData = [1, 2, 3, 4, 5];
......
The key difference between these two data types is "how they are stored in memory."
In memory, variables don't directly map to data values. Instead, they first map to a memory location, which then maps to the actual data, as illustrated below:
Note: The diagrams in this article are simplified for conceptual understanding. Actual memory operations are more complex.
The Stack
is a relatively small but quickly accessible memory space where variables are stored in a conceptual table including: "variable name," "memory location," and "data value."
As mentioned earlier, primitive types and object types are stored differently in memory:
- Primitive types: In the
Stack
, the actual data value is stored directly, as shown in the image above for primitive type variables. - Object types: In the
Stack
, only the memory address (location) of the data value is stored to serve as a reference. This reference points to the actual data value in theHeap
, as shown below.
The Heap
is a larger memory space compared to the Stack
, more suitable for storing larger data like objects, though access is relatively slower.
With this understanding of how the two data types are stored in memory, let's look at how variables are copied.
Copying Variables in Memory
When a variable is copied, the data in the Stack
is copied and a new memory location is created for the new variable, as shown in the following example and diagram:
// Copying primitive type data
let a = 5;
let b = a; // Copying variable a to b
First, we declare variable a
with memory location 0x001
and value 5
. Then we declare b = a
, effectively copying variable a
. We see that b
has a new location 0x02
, but its data value is the same as a
: 5
.
Now let's see how copying works with object data:
// Copying object data
let a = { number: 5 };
let b = a; // Copying variable a to b
When copying variables, we're still copying data in the Stack
. However, since object data stores the address of the value rather than the value itself, what gets copied is this address, not the original value. To summarize:
- Primitive types: When copying a variable, the original value is copied directly.
- Object types: When copying a variable, only the address is copied, and this address points to the same value.
This distinction in copying behavior is the key point of this article, and everything that follows builds on this concept!
Let's explore how these different copying behaviors affect our code.
First, let's observe what happens when we copy a primitive type variable and then modify the original variable:
// Copying primitive type data
let a = 5;
let b = a;
console.log(a); // 5
console.log(b); // 5
a = 10;
console.log(a); // 10
console.log(b); // 5 => unchanged, doesn't follow a's changes
This code does two main things:
- Declare variable
a
with value5
, then declare variableb
and copya
to it withb = a
. At this point, both print as5
. - Reassign variable
a
to10
and observe whether this affects the copied variableb
.
The result shows that the copied variable b
does not change when variable a
changes.
The reason is: When variable data is a primitive type, copying the variable creates a completely new "value".
So changing the original variable a
naturally doesn't affect variable b
(and vice versa), as illustrated:
Now that we've seen how primitive types behave when the original and copied variables change independently, let's observe objects:
// Copying object data
let a = { number: 5 };
let b = a;
console.log(a); // { number : 5 }
console.log(b); // { number : 5 }
a.number = 10;
console.log(a); // { number : 10 }
console.log(b); // { number : 10 } => changed with a
This code does two main things:
- Declare variable
a
with value{ number: 5 }
, then declare variableb
and copya
to it withb = a
. At this point, both print as{ number: 5 }
. - Change
a.number = 10
to make variablea
become{ number: 10 }
and observe whether this affects the copied variableb
.
The result clearly shows that the copied variable b
's object value changes along with variable a
, both becoming { number: 10 }
.
The reason is: When variable data is an object type, copying the variable copies the "address" not the value itself. The same address points to the same value.
Since both variables share the same address pointing to the same value, changing the original variable a
with a.number = 10
naturally affects variable b
(and vice versa), as illustrated:
To summarize again:
- When variable data is a primitive type, copying the variable creates a completely new "value."
- When variable data is an object type, copying the variable copies the "address" not the value itself. The same address points to the same value.
Now let's apply these concepts to function parameter passing!
Understanding Pass by Value Through Function Parameters
As the title suggests, we'll understand pass by value
through function parameter passing!
First, it's important to know that: passing parameters to a function behaves like copying variables.
Let's examine both primitive types and objects, starting with primitive types:
function test(primitiveData) {
primitiveData = primitiveData + 5;
console.log(primitiveData); // 10
}
let a = 5; // Primitive type data
test(a);
console.log(a); // 5 => unchanged
Let's analyze this code's execution flow:
- Declare
function test(primitiveData)
. - Declare variable
a
with primitive type value5
. - When passing
a
to thetest
function, it's equivalent toprimitiveData
copyinga
, likeprimitiveData = a
, creating a new local variableprimitiveData
in the function. - Since the variable data is a primitive type, copying the variable directly copies the "value", so variables
a
andprimitiveData
have independent values. - Therefore, changing one variable doesn't affect the other, so the final output of
a
remains unchanged at5
.
Here's the concept illustrated:
What we've just described is the concept of pass by value
.
Pass
refers to passing function parameters, and by value
means that when passing variables, the function copies the "value" of the passed variable. The result is that the function's internal variable value and the passed-in variable value are independent and don't affect each other.
Pass by value
can also be called call by value
, since functions can be called.
Understanding Pass by Reference Through Function Parameters
After understanding primitive types and pass by value
, let's examine object types and pass by reference
.
function test(objectData) {
objectData.number = 10; // Changing object content
console.log(objectData); // { number: 10 }
}
let a = { number: 5 }; // Object data
test(a);
console.log(a); // { number: 10 } => changed
Again, let's focus on the execution flow:
- Declare
function test(objectData)
. - Declare variable
a
with object type value{ number: 5 }
. - When passing
a
to thetest
function, it's equivalent toobjectData
copyinga
, likeobjectData = a
, creating a new local variableobjectData
in the function. - Since the variable data is an object type, copying the variable copies the "address" not the value, so
objectData
anda
have the same address pointing to the same value. - Therefore, changing
objectData.number = 10
modifiesobjectData
's object content, anda
is also modified, resulting in a final output of{ number: 10 }
.
Here's the concept illustrated:
This describes the concept of pass by reference
.
Pass
refers to passing function parameters, and by reference
means that when passing parameters, the function only copies the "address" as a reference coordinate to the actual value. The result is that when changing object content through objectData.number
or a.number
, the function's internal variable value and the passed-in variable value affect each other.
Of course, pass by reference
can also be called call by reference
.
Wait, What About Pass by Sharing?
Let's look at another example:
function test(objectData) {
objectData = { number: 10 }; // Reassigning the object
console.log(objectData); // { number: 10 }
}
let a = { number: 5 }; // object data
test(a);
console.log(a); // { number: 5 } => What?! It didn't change!
Again, focusing on the execution process:
- Declare
function test(objectData)
. - Declare variable
a
with object type value{ number: 5 }
. - When passing
a
to thetest
function, it's equivalent toobjectData
copyinga
, likeobjectData = a
, creating a new local variableobjectData
in the function. - Since the variable data is an object type, when we use
objectData = { number: 10 }
to reassign a value, a new address is created for the new object value, and objectData gets a new address pointing to the new value. - Therefore,
a
andobjectData
have different addresses pointing to different values, so the final output ofa
remains{ number: 5 }
, unchanged by the reassignment ofobjectData
.
The key difference from the previous example is that instead of changing the object's content with objectData.number = 10
, we're reassigning the entire object with objectData = { number: 10 }
.
Reassignment creates a new value { number: 10 }
in the Heap
memory, with a new corresponding address assigned to objectData
. In the end, objectData
has a new address pointing to a new value, independent from variable a
's address and value, as illustrated:
Doesn't this concept resemble pass by value
? Copying a variable also copies the actual value, so the two values are independent and don't affect each other.
So when we look at object data and how changing variable content after copying works, we see a mix of two behaviors:
- Pass by reference concept: After passing a parameter to a function, changing content with
object.number = 10
affects both variables because the external and internal variables share the same address pointing to the same value, so they affect each other. - Pass by value concept: After passing a parameter to a function, reassigning with
objectData = { number: 10 }
creates a new value and address. Since the external and internal variables have different addresses pointing to different values, they don't affect each other.
If objects were truly pass by reference
, reassignment would also affect the original variable, but it doesn't. Technically, this behavior is called pass by sharing
(also known as call by sharing
, call by object
, etc.).
Let's summarize how changing a "copied new variable (primitiveData or objectData above)" affects the "original variable (a above)" after copying (and vice versa):
-
For primitive types, the original variable "doesn't" change when the copied variable changes, showing
pass by value
behavior.function test(primitiveData) { primitiveData = primitiveData + 5; console.log(primitiveData); // 10 } let a = 5; // Primitive type data test(a); console.log(a); // 5 => unchanged
-
For object types, when only changing object content, the original variable "does" change when the copied variable changes, showing
pass by reference
behavior.function test(objectData) { objectData.number = 10; // Changing object content console.log(objectData); // { number: 10 } } let a = { number: 5 }; // Object data test(a); console.log(a); // { number: 10 } => changed
-
For object types, when reassigning the object, the original variable "doesn't" change when the copied variable changes, showing
pass by value
behavior.function test(objectData) { objectData = { number: 10 }; // Object reassignment console.log(objectData); // { number: 10 } } let a = { number: 5 }; // object data test(a); console.log(a); // { number: 5 } => unchanged
Combining these observations, one could say: In JavaScript, primitive type variables are pass by value
, while object variables are pass by sharing
.
Why Do Some Say JavaScript is All Pass by Value?
Let's revisit the concept table for copying both primitive and object variables:
If we ignore what's in the data field of the variable table - whether it's the original value or an address - and look at it intuitively, we're always copying "the value stored in the data field."
From this perspective: if we consider that what's being passed is always "the value stored in the data field", then it could be viewed as JavaScript being all pass by value. This is where that claim comes from.
Conclusion: Focus on How Variable Copying Works, Not Just Technical Terms
If you research numerous domestic and international sources, you'll find that there's no authoritative definition or description of these technical terms that can definitively prove which is absolutely correct.
- In JavaScript, primitive type variables are
pass by value
, while object variables arepass by sharing
. - JavaScript is all
pass by value
.
Both statements above can be considered correct, depending on how you define "value" and from which perspective you're looking.
I believe that the most important thing is the process of exploring these technical term definitions - this process has helped me better understand the behavior and results of "copying variables" in JavaScript, which is a common scenario in development, making it very practical.
If you want key points for easier memorization, here they are:
- For primitive type variables, like
const a = 5
, copying a variable copies the "original value," so the original and copied variables "don't" affect each other. - For object variables, like
const a = { number: 5 }
, copying a variable copies the "address," and when the same address points to the same value, the original and copied variables "do" affect each other. However, be aware that "reassignment" creates brand new addresses and values. - When passing an external variable as a parameter to a function, it means copying the external variable within the function's scope, creating a new internal variable.
These three points all describe "how the variable copying process works," which is the critical aspect. As long as you understand how variable copying works (visualized in the variable tables above), you'll clearly understand whether changes to original and copied variables will affect each other.
To recap the concrete results with code examples:
/*** Primitive types ***/
function test(primitiveData) {
primitiveData = primitiveData + 5;
console.log(primitiveData); // 10
}
let a = 5; // primitive data
test(a);
console.log(a); // 5 => unchanged
/*** Object types - changing content ***/
function test(objectData) {
objectData.number = 10; // Changing object content, no reassignment
console.log(objectData); // { number: 10 }
}
let a = { number: 5 }; // object data
test(a);
console.log(a); // { number: 10 } => changed
/*** Object types - reassignment ***/
function test(objectData) {
objectData = { number: 10 }; // Object reassignment
console.log(objectData); // { number: 10 }
}
let a = { number: 5 }; // object data
test(a);
console.log(a); // { number: 5 } => unchanged
Finally, I'll conclude with passages from two articles:
"In 'The Importance of Vocabulary and Common Language' from 'The World of Programming by Yukihiro Matsumoto,' the author discusses how determining appropriate terms for concepts aims to provide a common vocabulary for design and make developers aware of their existence - this is the true purpose of terminology." —"Technical Term Disputes"
Technical terms exist to facilitate communication and understanding concepts, not for arguments.
"Technical terms exist to describe concepts, not the other way around. What's most important is the concept they're trying to express - the resulting 'behavior'." —"JavaScript Things You Must Know #Day26: The Hamlet of Programming — Pass by value, or Pass by reference?"
This statement perfectly reflects my takeaway after organizing this article - we should focus more on the behavior and results exhibited when programs run.
I hope that after reading this article, you better understand the behavior of variable data during copying.