javascriptoopobject

Why does mutating an object assigned to another variable modify the original object? How to avoid it?


Suppose we have two objects:

var a = { foo: { bar: 1 } }
var b = { foo: { bar: 2 } }

If I set the object b to a (a = b), I expect that a takes the value of b, not the reference. So, in this case:

a = b
a.foo.bar = 3
console.log(b.foo.bar);

I expect that the last console.log shows 2, not 3. Why? Just because I changed a property related to a, not to b.

I can't understand why JavaScript also changes the b property and how to avoid this unexpected behaviour.

How to avoid this behaviour? Should I assign object to variables in a different way?


Solution

  • ...I expect that a takes the value of b, not the reference...

    That's incorrect. Variables contain values. In the case of an object, the value is an "object reference" which tells the JavaScript engine where the object is (elsewhere) in memory.¹ So a = b makes a "point" to the same object b "points" to. If you change that object's properties, you'll observe those changes regardless of the variable you get the reference from.

    After your initial setup, you have something like this in memory (various details omitted):

                   +−−−−−−−−−−−−−−+
    a: Ref5465−−−−>|   (object)   |
                   +−−−−−−−−−−−−−−+     +−−−−−−−−−−+
                   | foo: Ref8761 |−−−−>| (object) |
                   +−−−−−−−−−−−−−−+     +−−−−−−−−−−+
                                        | bar: 1   |
                                        +−−−−−−−−−−+
    
                   +−−−−−−−−−−−−−−+
    b: Ref1574−−−−>|   (object)   |
                   +−−−−−−−−−−−−−−+     +−−−−−−−−−−+
                   | foo: Ref4456 |−−−−>| (object) |
                   +−−−−−−−−−−−−−−+     +−−−−−−−−−−+
                                        | bar: 2   |
                                        +−−−−−−−−−−+
    

    (Those "ref" values are conceptual, of course; we never actually see them.)

    then when you do a = b, the object that a used to refer to is eligible for garbage collection and you have this instead:

    a: Ref1574−−+
                |
                |
                |  +−−−−−−−−−−−−−−+
                +−>|   (object)   |
                |  +−−−−−−−−−−−−−−+     +−−−−−−−−−−+
                |  | foo: Ref4456 |−−−−>| (object) |
                |  +−−−−−−−−−−−−−−+     +−−−−−−−−−−+
    b: Ref1574−−+                       | bar: 2   |
                                        +−−−−−−−−−−+
    

    Notice how a now has the same "ref" value b does. So naturally, a.foo.bar = 3 changes that one bar property both a and b (indirectly) point to.

    If you want to make a copy of an object, you can make a shallow copy with a = Object.assign({}, b) or (in ES2018+) a = {...b}. But if you want to copy the object foo refers to, you'll need a deep copy; see this question's answers.


    ¹ Some people, particularly in academic contexts, would say that the object itself is the value and that the "object reference" thing is a low-level plumbing concept, and the only reason we notice a difference between objects and, say, numbers is that objects are mutable. I understand their point, but I take a pragmatic approach to it instead.