Value vs Reference in JavaScript

JavaScript , Sun Jan 08 2023

    Value vs Reference

    JavaScript differentiates Data Types on:

    • Primitive values (Number, String, Boolean, Null, Undefined...)
    • Complex values (Objects, Arrays)

    Copying Primitive Values:

    When copying primitive values, JavaScript is going to behave as we expect it to. You just need to see what was the value of the variable at the time of the assignment.

    Let me give you a few real examples:

    Copying Numbers

    let x = 1;
    let y = x;

    x=2;

    console.log(x); // 2
    console.log(y); // 1

    Copying Strings

    let firstPerson = 'Mark';
    let secondPerson = firstPerson;

    firstPerson = 'Austin';

    console.log(firstPerson); // Austin
    console.log(secondPerson); // Mark

    Copying Complex Values:

    When copying complex values, JavaScript engine is not going to behave as you would initially think it would.

    Let me give you an example:

    Copying Arrays

    const animals = ['dogs', 'cats'];
    const otherAnimals = animals;

    animals.push('llamas');

    console.log(animals); // ['dogs', 'cats', 'llamas']
    console.log(otherAnimals); // ['dogs', 'cats', 'llamas']

    Wait what?! What happened here? Why are both arrays the same if we only pushed the value to the first array?

    Something just doesn't feel right, right?

    Let's try something similar with objects to see whether the behaviour continues.

    Copying Objects

    const person = {
        firstName: 'Jon',
        lastName: 'Snow',
    };
    const otherPerson = person;
    person.firstName = 'JOHNNY';
    console.log(person); // {firstName: 'JOHNNY', lastName: 'Snow' }
    console.log(otherPerson); // {firstName: 'JOHNNY', lastName: 'Snow' }

    What?! Again, this doesn't look right. So.... what happened here? Well...

    When a variable is assigned a primitive value, it just copies that value. We saw that with number and strings examples.

    On the other hand, when a variable is assigned a non-primitive value (such as an object, an array or a function), it is given a reference to that object's location in memory.

    What does that mean?

    In this example above, the variable otherPerson doesn't actually contain the value { firstName: 'Jon', lastName: 'Snow' }, instead it points to a location in memory where that value is stored.

    What?! Again, this doesn't look right. So.... what happened here? Well...

    const otherPerson = person;

    When a reference type value is copied to another variable, like otherPerson in the example above, the object is copied by reference instead of value. In simple terms, person & otherPerson don't have their own copy of the value. They point to the same location in memory.

    person.firstName = 'JOHNNY';
    console.log(person); // {firstName: 'JOHNNY', lastName: 'Snow' }
    console.log(otherPerson); // {firstName: 'JOHNNY', lastName: 'Snow' }

    When a new item is pushed to person, the array in memory is modified, and as a result the variable otherPerson also reflects that change.

    We're never actually making a copy of a person object. We're just make a variable that points to the same location in the memory.

    Equality

    We can prove that with a simply equality check.

    const person = { firstName: 'Jon'};
    const otherPerson = { firstName: 'Jon'};
    console.log(person === otherPerson);

    What do you think? Are person and otherPerson equal? Well, they should be, right?

    They look exactly the same, have the same keys and values. Let's check it out:

    console.log(person === otherPerson); //false

    You might expect person === otherPerson to resolve to true but that isn't the case. The reason behind that is that although person and otherPerson contain identical objects, they still point to two distinct objects stored in different locations in memory.

    Now, let's create a copy of the person object by copying the object itself, rather than creating a completely new instance of it.

    anotherPerson = person;
    console.log(person === anotherPerson); // TRUE

    person and anotherPerson hold reference to the same location in memory & are therefore considered to be equal.

    Awesome! We just learned that primitive values are copied by value, and that objects are copied by reference.

    Up next we're going to learn how to make a real copy of an object. That will allow us to copy an object and change it without being afraid that we'll change both objects at the same time.

    Shallow Cloning

    We've seen all the problems we could possibly encounter if we tried changing values of an objet copied by a reference. So how can we properly copy it and remove a reference? How can we create a complete clone of the object?

    Cloning Arrays:

    1st way: Spread Operator

    Spread operator is a newer addition to JavaScript. Before using it to clone an object without keeping a reference.

    Let's first explore how it works.

    Imagine you had an array:

    const numbers = [1, 2, 3, 4, 5];
    const newNumbers = [...numbers];

    How could we use the spread operator on this array? Spread syntax allows us to "spread" the values of strings, objects and arrays.

    How does it work? Let's see it in action on our numbers array.

    The syntax of a spread operator is represented just by three dots.

    console.log(...numbers); // 1, 2, 3, 4, 5

    We get back all the values from the array individually, one after the other, they got taken out of the array. So how can we make use of this?

    Take a look at the following code. In there, we created a new variable, and in it we put a completely new, empty array, in which we spread the values of our original array.

    const newNumbers = [...numbers];

    Let's do a simple console.log:

    console.log(newNumbers); // [1, 2, 3, 4, 5]

    We get an array that looks identical to the one that we had at the beginning, but is it completely identical? Let's check the equality. We're going to create another copy and then compare them.

    const copiedNumbers = numbers;
    console.log(numbers === copiedNumbers); // true
    console.log(numbers === newNumbers); // false

    We got back true and false. What does this mean? This means that the copiedNumbers array is pointing to the same place in memory where the original numbers array is pointing to.

    Therefore, if we change one, or the other, they would both change. And we do not want that.

    On the other hand, numbers and newNumbers are not the same. They represent a completely different array. Let's try changing the original numbers array:

    numbers.push(6); console.log(numbers); // [1, 2, 3, 4, 5, 6]
    console.log(copiedNumbers); // [1, 2, 3, 4, 5, 6]
    console.log(newNumbers); // [1, 2, 3, 4, 5]

    As you can see, the numbers & copiedNumbers both got changed. And the array with we created using the spread operator was unchanged! This means that we created something called a "shallow clone". The "shallow" part is going to make sense once we introduce the "deep clones".

    2nd way: Array.slice()

    numbers = [1, 2, 3, 4, 5];
    numbersCopy = numbers.slice();
    // [1, 2, 3, 4, 5]

    Cloning Objects

    1st way: Spread Operator

    We can clone the objects using the spread operator as well.

    const person = {
        name: 'Jon',
        age: 20,
    };
    const otherPerson = {...person};

    ... now we can change the otherPerson without changing person:

    otherPerson.age = 21;
    console.log(person); // unchanged
    console.log(otherPerson); // changed

    2nd way: Object.assign()

    var A1 = { a : '2'};
    var A2 = Object.assign({}, A1);

    Awesome! We just learned two different ways for cloning both objects and arrays! As I mentioned, the ways of cloning we just explored create SHALLOW CLONES.

    const person = {
        firstName: 'Emma',
        car: {
            brand :'BMW',
            color: 'blue',
            wheels: 4,
        },
    };

    Now.. Let's try creating a copy of that object, in one of the ways we've learned now:

    For example, we can use the spread operator:

    const newPerson = {...person};

    Great, we've learned that this removes the reference from the original object, right?

    So let's try changing the newly created object:

    newPerson.firstName = 'Mia';

    Let's console logging both objects and see what changed? Without reading you can try to answer.

    console.log(person); // unchanged
    console.log(newPerson); // changed

    As we learned, if we use the spread operator, the reference to the initial object gets deleted, therefore, we can change the new object without having to worry!

    Oh, if only it were that simple... Let's see what would happen if we tried changing the properties of Mia's car.

    newPerson.car.color = 'red';

    ... and then console log both objects:

    console.log(person); // changed
    console.log(newPerson); // unchanged

    Both objects got changed. How did that happen?

    Well... we only remove the reference from the outer object, the person one, but notice how the car is also an object...

    It has it's own reference, and the same rules apply.. If we want a real copy, we need to remove a reference from the inner object as well.

    We could do that by spreading the properties of an inner object:

    const newPerson = { ...person, car: { ...person.car }};

    If we now tried changing the properties of the car, it would only change them on the newly created object, let's try it out:

    newPerson.car.color = 'red';
    console.log(person); // unchanged
    console.log(newPerson); // changed

    That's great! But this only works for two levels into depth, first level is when we're inside of the person object, and second level is when we're inside of the car object.

    But if we had more nested objects, we'd need to spread everything. And that's not the solution. When we have deeply nested objects, we need to create a deep clone. For an object to be a deep clone, it needs to destroy all the references.

    There are two methods that are going to make this extremely easy for us. First one is called JSON.strinfigy and the second one is called JSON.parse. Let's see them in action, we can use the same person object we declared above:

    const person = {
        firstName: 'Emma',
        car: {
            brand :'BMW',
            color: 'blue',
            wheels: 4,
        },
    };

    ... first, we're going to use JSON. stringify method. The JSON.stringify() method converts a JavaScript object or any value to a string. During this process, all the references are destroyed.

    const stringifiedPerson = JSON.stringify(person);
    console.log(stringifiedPerson);

    The thing that we get back is a string... That isn't all that valuable to us. How do we turn it back to an object again? We can do that by using JSON.parse method:

    const newPerson = JSON.parse(stringifiedPerson);
    console.log(newPerson);

    The JSON.parse() method parses a string, constructing the JavaScript value or object contained in the string.

    Before we start testing it out, there's one simple tweak we can make. We can do it in one line:

    const newPerson = JSON.parse(JSON.stringify(person));

    Awesome! We got our object back. Let's prove that all of the references are indeed deleted and that the newPerson is indeed a deep clone.

    newPerson.firstName = 'Mia';
    newPerson.car.color = 'red';
    console.log(person); // unchanged
    console.log(newPerson); // changed

    That's it! You've just mastered one of the hardest topics in the whole of JavaScript! If you're still a bit confused, that's completely normal.

    I'd suggest rereading this section. Take it slow. Mastery takes time.