Q: Is JavaScript call-by-value or call-by-reference?
A: JavaScript is strictly call-by-value.
Q: How can that be when a majority of the types in JavaScript are implemented as references?
A: Both call-by-value and call-by-reference (termed evaluation strategies), have absolutely nothing to do with how variables are represented or communicated within a language. Evaluation strategy instead defines the extent to which a function is able to manipulate the environment from which it was called through the arguments it has received.
Evaluation Strategy
When you call a function, the way arguments are handled has a huge impact on how we, as developers, are able to reason about the state of the environment after a function has been called.
The Wikipedia entry for Evaluation Strategy is long, confusing and sometimes contradictory because the various terms have been inconsistently applied to languages throughout the years.
The most important distinguishing features of call-by-value is:
If the function is able to assign values to its parameters, only its local copy is assigned — that is, anything passed into a function call is unchanged in the caller's scope when the function returns.
In call-by-value, a function can't make modifications to the calling environment by direct assignment to the arguments it has received. Everything in the argument list is a copy. Inside the function, any assignment to an argument changes the argument only for the remaining duration of the function body. From the point-of-view of the calling environment, no assignment was ever made.
Compare that to the definition of call-by-reference:
[call by reference] typically means that the function can modify (i.e. assign to) the variable used as argument — something that will be seen by its caller.
How JavaScript Works
The definition for call-by-value is exactly how arguments within JavaScript functions operate. No matter how you attempt to assign a new value to an argument, that change will never be visible from outside the function.
The example below clearly shows this behavior:
let primitive = 1;
let object = { foo: 'bar' };
let primitive_copy = primitive;
let object_reference = object;
const assign_to_arguments = (in_primitive, in_reference) => {
in_primitive = 2;
in_reference = { foo: 'baz' };
};
assign_to_arguments(primitive, object);
primitive === primitive_copy; //=> true
object === object_reference; //=> true
There is simply no way to have the conditions at bottom of the snippet evaluate to false in JavaScript without manipulating the closed-over variables directly. It is impossible to alter the value of a function's arguments in a way that is visible to the calling environment.
What About Objects!?
"Aha!", says the JavaScript developer, "But I can change objects and that change is visible to the calling environment because objects are passed-by-reference!"
Sorry, this particular behavior does not mean that JavaScript is call-by-reference. This common misconception is the result of conflating the argument's value for what the argument references.
While it is true to say that JavaScript is passing your function a reference to an object, that reference itself is not the same reference that existed outside of the function. The reference itself is passed by value - your functions only ever receive a copy of the original references.
In other words, JavaScript treats references to objects just like values and those values are copied just like any other value when a function is called with an object for an argument.
That Other call-by-value Language
Let's take a moment to look at an unquestionably call-by-value language: C.
C takes a somewhat more strict approach to call-by-value and copies everything including structures and arrays on the stack. Since JavaScript has no direct analogue to C's stack-based structures or arrays, that particular case isn't important. In JavaScript, references to objects and arrays behave like C's pointers to structures and arrays on the heap.
Like everything else in C's call-by-value semantics, pointers are copied as well. One of the effects of this copying is that changes to a pointer's value are not visible to the outside world. On the other hand, the memory to which that pointer refers is clearly still mutable and those mutations are visible outside of the function.
The key point here is: no one will ever attempt to argue that C is call-by-reference simply because you are able to mutate whatever region of memory a pointer references. What matters is that the value of the pointer has been copied and that any changes to that pointer's value itself are not visible to the environment from which the function was called.
Before We Go
There is one more interesting detail to consider. In C, we can emulate a call-by-reference system by passing pointers for value-types and pointers-to-pointers for reference-types (on the heap). In that way it is possible, though quite laborious, to explicitly implement call-by-reference semantics in C.
This particular method of implementing a call-by-reference system is not available to us in JavaScript-land because the language's internal "reference values" are completely out of the reach of the developer.
The end result is that JavaScript is so strictly call-by-value that it is impossible to implement a call-by-reference evaluation strategy in JavaScript even if one so desired!