Value vs. Reference
In many other languages, values can either be assigned/passed by value-copy or by reference-copy depending on the syntax you use.
For example, in C++ if you want to pass a number
variable into a function and have that variable’s value updated, you can declare the function parameter like int& myNum
, and when you pass in a variable like x
, myNum
will be a reference to x
; references are like a special form of pointers, where you obtain a pointer to another variable (like an alias). If you don’t declare a reference parameter, the value passed in will always be copied, even if it’s a complex object.
In JavaScript, there are no pointers, and references work a bit differently. You cannot have a reference from one JS variable to another variable. That’s just not possible.
A reference in JS points at a (shared) value, so if you have 10 different references, they are all always distinct references to a single shared value; none of them are references/pointers to each other.
Moreover, in JavaScript, there are no syntactic hints that control value vs. reference assignment/passing. Instead, the type of the value solely controls whether that value will be assigned by value-copy or by reference-copy.
Let’s illustrate:
var a = 2;
var b = a; // `b` is always a copy of the value in `a`
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // `d` is a reference to the shared `[1,2,3]` value
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
Simple values (aka scalar primitives) are always assigned/passed by value-copy: null
, undefined
, string
, number
, boolean
, and ES6’s symbol
.
Compound values — object
s (including array
s, and all boxed object wrappers — see Chapter 3) and function
s — always create a copy of the reference on assignment or passing.
In the above snippet, because 2
is a scalar primitive, a
holds one initial copy of that value, and b
is assigned another copy of the value. When changing b
, you are in no way changing the value in a
.
But both c
and d
are separate references to the same shared value [1,2,3]
, which is a compound value. It’s important to note that neither c
nor d
more “owns” the [1,2,3]
value — both are just equal peer references to the value. So, when using either reference to modify (.push(4)
) the actual shared array
value itself, it’s affecting just the one shared value, and both references will reference the newly modified value [1,2,3,4]
.
Since references point to the values themselves and not to the variables, you cannot use one reference to change where another reference is pointed:
var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// later
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
When we make the assignment b = [4,5,6]
, we are doing absolutely nothing to affect where a
is still referencing ([1,2,3]
). To do that, b
would have to be a pointer to a
rather than a reference to the array
— but no such capability exists in JS!
The most common way such confusion happens is with function parameters:
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// later
x = [4,5,6];
x.push( 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [1,2,3,4] not [4,5,6,7]
When we pass in the argument a
, it assigns a copy of the a
reference to x
. x
and a
are separate references pointing at the same [1,2,3]
value. Now, inside the function, we can use that reference to mutate the value itself (push(4)
). But when we make the assignment x = [4,5,6]
, this is in no way affecting where the initial reference a
is pointing — still points at the (now modified) [1,2,3,4]
value.
There is no way to use the x
reference to change where a
is pointing. We could only modify the contents of the shared value that both a
and x
are pointing to.
To accomplish changing a
to have the [4,5,6,7]
value contents, you can’t create a new array
and assign — you must modify the existing array
value:
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// later
x.length = 0; // empty existing array in-place
x.push( 4, 5, 6, 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [4,5,6,7] not [1,2,3,4]
As you can see, x.length = 0
and x.push(4,5,6,7)
were not creating a new array
, but modifying the existing shared array
. So of course, a
references the new [4,5,6,7]
contents.
Remember: you cannot directly control/override value-copy vs. reference — those semantics are controlled entirely by the type of the underlying value.
To effectively pass a compound value (like an array
) by value-copy, you need to manually make a copy of it, so that the reference passed doesn’t still point to the original. For example:
foo( a.slice() );
slice(..)
with no parameters by default makes an entirely new (shallow) copy of the array
. So, we pass in a reference only to the copied array
, and thus foo(..)
cannot affect the contents of a
.
To do the reverse — pass a scalar primitive value in a way where its value updates can be seen, kinda like a reference — you have to wrap the value in another compound value (object
, array
, etc) that can be passed by reference-copy:
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a: 2
};
foo( obj );
obj.a; // 42
Here, obj
acts as a wrapper for the scalar primitive property a
. When passed to foo(..)
, a copy of the obj
reference is passed in and set to the wrapper
parameter. We now can use the wrapper
reference to access the shared object, and update its property. After the function finishes, obj.a
will see the updated value 42
.
It may occur to you that if you wanted to pass in a reference to a scalar primitive value like 2
, you could just box the value in its Number
object wrapper (see Chapter 3).
It is true a copy of the reference to this Number
object will be passed to the function, but unfortunately, having a reference to the shared object is not going to give you the ability to modify the shared primitive value, like you may expect:
function foo(x) {
x = x + 1;
x; // 3
}
var a = 2;
var b = new Number( a ); // or equivalently `Object(a)`
foo( b );
console.log( b ); // 2, not 3
The problem is that the underlying scalar primitive value is not mutable (same goes for String
and Boolean
). If a Number
object holds the scalar primitive value 2
, that exact Number
object can never be changed to hold another value; you can only create a whole new Number
object with a different value.
When x
is used in the expression x + 1
, the underlying scalar primitive value 2
is unboxed (extracted) from the Number
object automatically, so the line x = x + 1
very subtly changes x
from being a shared reference to the Number
object, to just holding the scalar primitive value 3
as a result of the addition operation 2 + 1
. Therefore, b
on the outside still references the original unmodified/immutable Number
object holding the value 2
.
You can add properties on top of the Number
object (just not change its inner primitive value), so you could exchange information indirectly via those additional properties.
This is not all that common, however; it probably would not be considered a good practice by most developers.
Instead of using the wrapper object Number
in this way, it’s probably much better to use the manual object wrapper (obj
) approach in the earlier snippet. That’s not to say that there’s no clever uses for the boxed object wrappers like Number
— just that you should probably prefer the scalar primitive value form in most cases.
References are quite powerful, but sometimes they get in your way, and sometimes you need them where they don’t exist. The only control you have over reference vs. value-copy behavior is the type of the value itself, so you must indirectly influence the assignment/passing behavior by which value types you choose to use.