The way JavaScript handles the passing of variables is something fundamental to understand if you're developing in JavaScript as it can lead to some unexpected behaviour in some cases.

I have posted a tweet with a poll about exactly this behaviour and now I'm going to explain, why "2" this is the output and some additional examples you should watch out for.

TL;DR;

In JavaScript, everything is being passed as values. The difference from primitives to objects is, that with objects you are passing a reference to an object in memory (as value) instead of passing the object directly.

The longer version

Let's first get into some basic topics on how variable passing is handled for primitives vs. how it's handled for non-primitives and what hoisting in JavaScript is.

Primitives

Primitives (string, number, boolean, null, undefined, symbol) are written directly into the variable. This means, that whenever we return a primitive from a function or assign a variable (which contains a primitive) to another one, we are actually copying that variable instead of passing/saving the reference to the original one:

let a = 1;
let b = a;
a = 2;

console.log(a); // 2
console.log(b); // 1

Non-Primitives

On the other hand, the non-primitive object is not written directly into the variable, but only the value of where that object is located in memory is placed in the variable (the pointer to the object in memory).

This means, that if we assign a new object literal to the variable, we are actually replacing the object with a new one (which will point to a different one in memory) and it looses reference to the original one. That's a similar behaviour as with primitive values.

let a = { name: 'Ivan' };
let b = a;
a = { name: 'Not Ivan' };

console.log(a); // Not Ivan
console.log(b); // Ivan

But if we just edit a property on the object, we are editing the original, referenced object. In the following example, the variable b contains the same address to an object as the variable a. That way, if we modify a property of the object inside the variable a without replacing the whole object literal, the changes will also be applied to the object, which the variable b points to (as both a and b are pointing to the same object in memory, as long as you don't reassign a different object).

let a = { name: 'Ivan' };
let b = a;
a.name = 'Maybe Ivan?';

console.log(a); // Maybe Ivan?
console.log(b); // Maybe Ivan?

Hoisting

Hoisting can be thought of as moving function and variable declarations to the top of your code. During compile time, your declarations will be put into memory, your code will physically still remain in the same place as you have written it. This gives us the possibility to access functions and variables in our code before they have been actually defined. You can read more about hoisting in this MDN article. The article also explains greatly, why this only works for declarations and not for assignments or initializations.

log('Hi there, this works'); // Hi there, this works

function log(message) {
  console.log(message)
}

The example

Now, let's dig into the example and go through it line by line:

function magic() {
  let a = 1;
  a = 2;
  let b = innerMagic();
  a = 3;

  return b;

  function innerMagic() {
    return a;
  }
}

console.log(magic()); // 2

Now let's go through how this code get's executed:

  • Lines 1-12 define our function magic, which will be parsed before executing the rest of the code.
  • Inside that function, hoisting will take effect. That's why the function innerMagic can be used in code before the function has been declared.
  • Line 2 defines the variable a and assigns the value "1" to it. Right now, a equals to "1".
  • Line 3 assigns the value "2" to the variable a. Right now, a equals to "2".
  • Line 4 then calls the innerMagic() function (which is already available due to hoisting) and assigns the returned value to the variable b. As the variable a is set to "2" at the time of calling the function, innerMagic() will return "2". Right now, a equals to "2" and b also equals to "2".
  • Line 5 then assigns the value "3" to the variable a. And as we learned above, primitives are saved directly into the variable (not as a reference), that's why right now a equals to "3" and b still equals to "2", as the value has been copied on line 4.
  • Line 7 lastly returns our value of the variable b, which is still set to "2".

Object example

Now we have seen the example with primitives, let's rewrite it to use objects and show, where pitfalls can occur:

function magic() {
  let a = { name: 'Ivan' };
  a.name = 'Julian';
  let b = innerMagic();
  a.name = 'Kilian';

  return b;

  function innerMagic() {
    return a;
  }
}

console.log(magic()); // { name: 'Kilian' }

Now let's go through how this code get's executed:

  • Lines 1-12 define our function magic, which will be parsed before executing the rest of the code.
  • Inside that function, hoisting will take effect. That's why the function innerMagic can be used in code before the function has been declared.
  • Line 2 defines the variable a and assigns the object "{ name: 'Ivan' }" to it. Right now, a equals to "{ name: 'Ivan' }".
  • Line 3 changes the name property of the variable a to "Julian". Right now, a equals to "{ name: 'Julian' }".
  • Line 4 then calls the innerMagic() function (which is already available due to hoisting) and assigns the returned value to the variable b. As the variable a is an object at the time of calling the function, innerMagic() will return the address to the object which a points to. Right now, a equals to "{ name: 'Julian' }" and b also equals to "{ name: 'Julian' }". They both point to the same object in memory.
  • Line 5 then changes the property name of the variable a to "Kilian". And as we learned above, objects are not saved directly into the variable, but instead the address to the object in memory is saved, that's why right now a equals to "{ name: 'Kilian' }" and b also equals to "{ name: 'Kilian' }", as both variables still point to the same object in memory and we've just modified the actual object in memory and not the content of the variable itself. The address still remains the same in both variables.
  • Line 7 lastly returns our value of the variable b, which is still set to "{ name: 'Kilian' }".

If you have any further questions, aspects you are unsure with or found anything, that is incorrect in this article, don't hesitate to leave a comment below or reach out to me on Twitter: @ivansieder