Preventing Objects from mutating
You can prevent objects from mutating through two methods:
Object.assign
assignment
Object.assign
Object.assign
lets you combine two (or more) objects into a single one. It has the following syntax:
const newObject = Object.assign(object1, object2, object3, object4)
newObject
will contain properties from every object you pass into Object.assign
.
const papayaBlender = { canBlendPapaya: true }
const mangoBlender = { canBlendMango: true }
const fruitBlender = Object.assign(papayaBlender, mangoBlender)
console.log(fruitBlender)
// {
// canBlendPapaya: true,
// canBlendMango: true
// }
If two conflicting properties are found, the property in a later object overwrites the property in an earlier object.
const smallCupWithEar = {
volume: 300,
hasEar: true
}
const largeCup = { volume: 500 }
// In this case, volume gets overwritten from 300 to 500
const myIdealCup = Object.assign(smallCupWithEar, largeCup)
console.log(myIdealCup)
// {
// volume, 500,
// hasEar: true
// }
But beware! When you combine two objects with Object.assign
, the first object gets mutated. Other objects don’t get mutated.
console.log(smallCupWithEar)
// {
// volume, 500,
// hasEar: true
// }
console.log(largeCup)
// {
// volume, 500
// }
Preventing Object.assign from mutating objects
You can pass a new object as your first object to prevent existing objects from mutating. You’ll still mutate the first object (which is empty), but that’s OK since this mutation doesn’t affect other objects—it won’t change external state.
const myIdealCup = Object.assign({}, smallCupWithEar, largeCup)
But Object.assign copies references to nested objects
The problem with Object.assign
is that it performs a shallow merge—it copies properties directly from one object to another. When it does so, it also copies references to any objects.
Let’s explain this statement with an example.
Suppose you buy a new sound system. The system lets you change three settings:
- Power (turned on or off)
- Volume (through a number)
- Loudness (through a number)
const defaultSettings = {
power: true,
soundSettings: {
volume: 50,
bass: 20,
}
}
You want to invite your friends to a party. You know some of them love loud music, so you create a preset that’s guaranteed to wake your neighbors.
const loudPreset = {
soundSettings: {
volume: 100
}
}
When you invite your friends over to the party, you want to preserve your existing presets (so you can switch back quickly). You choose to combine loudPreset
with the default settings.
const partyPreset = Object.assign({}, defaultSettings, loudPreset)
But partyPreset
sounds weird. The volume is loud enough, but the bass is non-existent. When you inspect partyPreset
, you’re surprised to find that there’s no bass in it!
console.log(partyPreset)
// {
// power: true,
// soundSettings: {
// volume: 100
// }
// }
This happens because JavaScript copies the reference to the soundSettings
object. Since both defaultSettings
and loudPreset
have a soundSettings
object, the one that comes later gets copied into the new object.
If you change partyPreset
, loudPreset
will mutate accordingly—evidence that partyPreset
links to loud preset’s soundSettings
object.
partyPreset.soundSettings.bass = 50
console.log(loudPreset)
// {
// soundSettings: {
// volume: 100,
// bass: 50
// }
// }
You need another method to ensure nested objects (objects in objects) don’t get passed over to partyPreset
. Enter assignment.
Assignment
assignment
is a library made by Nicolás Bevacqua from Ponyfoo, which is a great source for JavaScript knowledge. It helps you perform a deep merge without having to worry about mutation. Aside from the method name, the syntax is the same as Object.assign
.
// Perform a deep merge with assignment
const partyPreset = assignment({}, defaultSettings, loudPreset)
console.log(partyPreset)
// {
// power: true,
// soundSettings: {
// volume: 100,
// bass: 20
// }
// }
//
assignment
copies over values of all nested objects, which prevents your existing objects from getting mutated.
If you try to change any property in partyPreset.soundSettings
now, you’ll see that loudPreset
remains as it was.
partyPreset.soundSettings.bass = 50
// loudPreset doesn't get mutated
console.log(loudPreset)
// {
// soundSettings {
// volume: 100
// }
// }
Note: assignment
is just one of many libraries that help you perform a deep merge. Other libraries, including lodash.mereg and merge-options), can help you do it too. Feel free to choose from any of these libraries.
Should you always use assignment over Object.assign?
You can use Object.assign
if you’re not merging objects with nested objects. There’s no harm in using it as long as you know what you’re doing.
If you need to assign objects with nested properties, always use assignment
over Object.assign
.
Wrapping up
Object.assign
can be used to prevent objects from mutating.
Take note that Object.assign
can only prevent direct properties from mutating. If you need to merge objects with nested properties, use assignment
(or other libraries that do the same thing) instead of Object.assign
.
Exercise
- Combine two objects with
Object.assign
.
- Combine two objects with
assignment
.