Preventing Objects from mutating

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

Preventing Objects from mutating

You can prevent objects from mutating through two methods:

  1. Object.assign
  2. 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:

  1. Power (turned on or off)
  2. Volume (through a number)
  3. 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

  1. Combine two objects with Object.assign.
  2. Combine two objects with assignment.

Answers

  • Combine two objects with Object.assign.
const object1 = { wheels: 4 }
const object2 = { type: 'car' }

const merged = Object.assign({}, object1, object2)
  • Combine two objects with assignment.
// From https://github.com/bevacqua/assignment/blob/master/assignment.js
function assignment (result) {
  var stack = Array.prototype.slice.call(arguments, 1);
  var item;
  var key;
  while (stack.length) {
    item = stack.shift();
    for (key in item) {
      if (item.hasOwnProperty(key)) {
        if (typeof result[key] === 'object' && result[key] && Object.prototype.toString.call(result[key]) !== '[object Array]') {
          if (typeof item[key] === 'object' && item[key] !== null) {
            result[key] = assignment({}, result[key], item[key]);
          } else {
            result[key] = item[key];
          }
        } else {
          result[key] = item[key];
        }
      }
    }
  }
  return result;
}

const object1 = { wheels: 4 }
const object2 = { type: 'car' }

const merged = assignment(object1, object2)