In JavaScript, there are fundamental differences in storing and copying between objects and primitives. You can copy primitive values such as strings, numbers, or booleans just using =, but objects are stored in memory like references, so you can't duplicate them in the same way. In this topic, you will learn about some methods which can help you with this problem.
Reference type
What is a reference type? Let's start with primitives. Here we put a copy of skill into myStack.
let skill = 'JavaScript';
let myStack = skill;
Now you have two independent variables, which means that if you want to change one of the variables above, the other won't change.
// above code is used here
let skill = 'HTML';
console.log(skill); // 'HTML'
console.log(myStack); // 'JavaScript'
Quite simple, isn't it? As for objects, the situation is different. Let's look at an example:
You have two objects, developer and student, but what happens if you try to change a property in one of them?
let developer = { name: 'John', position: 'Frontend' };
let student = developer;
student.name = 'Karl';
console.log(student); // { name: 'Karl', position: 'Frontend' }
console.log(developer); // { name: 'Karl', position: 'Frontend' }
You wanted to change one property only in the object student, but the same property in the developer has has also been changed. Why is that?
Here we got to the definition of a reference type. An object is stored somewhere in memory, and a variable (like developer) has a reference to it. That's why here you don't duplicate the object but just copy the reference to it. If you try to change one of the object's properties, that property will change on both variables. That's because objects are reference types (so are arrays and functions). And when you use =, it copies the pointer to the memory space it occupies. So, the variables of reference types don't actually contain the value — they are just pointers to the value in memory.
It's also essential for comparison: objects are equal only if they have the same reference or, in other words, if it's just one object.
In our previous example, you copy a reference (not an object), and that's why these two variables are equal.
// look at the code above
console.log(developer === student); // true
At first glance, here you have two identical objects a and b: they are both empty but have different references, so they are not equal.
let a = {};
let b = {};
console.log (a === b); // false
So, using = you can copy only primitives values, whereas in the case of reference types you just copy its reference. Reference types will be equal only if you compare two identical references.
let c = {};
let d = c;
console.log(c === d); // true
However, there are a few methods that can help you copy objects. Let's have a look at them!
Using spread
You can clone your object using the spread operator. This method unpacks the properties of one object into another:
const developer = {
name: 'John',
age: 26,
};
const student = { ...developer };
console.log(student);
/*
{
name: 'John',
age: 26,
};
*/
Now you have an actual copy of the object, not just a reference copy. And these two objects are independent, which you can check:
// above code is used here
console.log(developer === student); // false
student.name = 'Ben';
console.log(developer.name, student.name); // 'John' 'Ben'
As you can see, the objects are not equal, so they have different references, and when you change one of the properties, it doesn't affect the other variable.
Let's look at a more complex object like this:
// man object
let man = {
name: "Jack",
age: 28,
mother: {
name: "Kate",
age: 50,
},
};
This object has not only primitives properties (like name and age), but also a property mother, which is also an object. Such objects are called nested objects. In other words, they are objects inside other objects: an object can have another object, array, or function (method) as a property. Let's copy this object using spread:
// man object is used here
let manCopy = { ...man };
console.log(man === manCopy); // false
manCopy.age = 42;
console.log(man.age, manCopy.age); // 28 42
On primitives, it works as expected. Let's take a look at the mother property:
// above code is used here
console.log(man.mother === manCopy.mother); // true
manCopy.mother.age = 67;
console.log(man.mother); // { name: 'Kate', age: 67 }
console.log(manCopy.mother); // { name: 'Kate', age: 67 }
Here we have the same problem again: the spread operator clones only primitives, but if you have reference type properties, this method copies only their references. It is called a shallow copy. This means both variables point to the same object in memory. As a result, if the state of the object changes through any of the reference variables, it is reflected in both.
So, when you clone a nested object using spread, properties that are reference types (such as objects, arrays, or functions) copy only their references. But you can use spread on all references, including properties. For this, you must find the exact path for each property that has reference types:
// man object is used here
manFullCopy = { ...man, mother: { ...man.mother }};
console.log(man === manFullCopy); // false
console.log(man.mother === manFullCopy.mother); // false
In this example, the object is located in the variable man(given above in the topic). The property mother is also an object, and it is located in man.mother. You need to use the spread operator for all references. First, you name the property of an object and then use {...object.property}.
Take a look at another example:
const family = {
child: {
name: "Jack",
age: 5,
},
father: {
name: "Kevin",
age: 30,
},
mother: {
name: "Jess",
age: 30,
},
};
console.log(family.child); //{name: 'Jack', age: 5}
console.log(family.father); //{name: 'Kevin', age: 30}
console.log(family.mother); //{name: 'Jess', age: 30}
family is an object with 3 properties: child, father, and mother. In turn, these properties are also objects, they are references and their addresses are family.child, family.father, family.mother respectively. So to get a full copy of family, you should copy all these references:
// above code is used here
const deepFamilyCopy = {
child: { ...family.child },
father: { ...family.father },
mother: { ...family.mother },
};
Looks quite complicated, doesn' it? And what if you have an object like this:
const complicatedMan = {
name: 'John',
age: 28,
mother: {
name: 'Kate',
age: 50,
work: {
position: 'doctor',
experience: 15,
address: {
street: 'Park Avenue',
house: 1
}
}
}
};
Even if you are well aware of how to use spread, it might all still be confusing.
For copying such nested objects, you should use not shallow copying but deep copy methods. A deep copy of an object has properties that don't share the same references as the source object from which the copy was made. You will learn about these methods in the next topic.
The spread operator is perfect for simple non-nested objects, but it's not the most appropriate if you have more complex objects. Let's see what to do in such cases!
You can see the full copy of the previous example below. Not so clear and readable, is it?
// above code is used here
const complicatedManFullCopy = { ...complicatedMan,
mother: { ...complicatedMan.mother,
work: { ...complicatedMan.mother.work,
address: { ...complicatedMan.mother.work.address }
}
}
};
Using Object.assign
This method also allows getting a shallow copy of the object. The syntax goes like this:
Object.assign(targetObj, source1, source2, ...etc);
The first argument is a target object (it can be just an empty object {}). Further arguments are a list of objects, and all their properties are copied into the target object. For example:
const man = { name: 'Ross' };
const property1 = { surname: 'Geller' };
const property2 = { occupation: 'Paleontologist' };
Object.assign(man, property1, property2);
console.log(man); // { name: 'Ross', surname: 'Geller', occupation: 'Paleontologist' }
Or if you need to copy one object, you can do this:
const woman = { name: 'Monica', surname: 'Geller' };
const cloneWoman = Object.assign({}, woman);
console.log(cloneWoman); // { name: 'Monica', surname: 'Geller' }
console.log(woman === cloneWoman); //false
Note the empty object {} as the first argument — this will ensure you don't mutate the original object.
This method allows uniting several objects into one, but it works like spread with nested objects. It also makes only a shallow copy:
// above codes are used here
const family = { brother: man };
Object.assign(woman, family);
console.log(woman);
/*
{
name: 'Monica',
surname: 'Geller',
brother: {
name: 'Ross',
surname: 'Geller',
occupation: 'Paleontologist'
}
}
*/
console.log(woman.brother === family.brother); //true
console.log(woman.brother === man); //true
console.log(man === family.brother); //true
As you can see, all these objects are references to one, and that's why they are equal.
Conclusion
Objects are assigned and copied by reference, and are stored as a "reference" (address in memory) for the value. So, copying or passing such a variable as a function argument copies only that reference, not the object itself. To duplicate an object, you can use Object.assign() or spread { ...object } for the shallow copy. However, be aware that there are some problems with cloning nested objects.