Understanding Object.assign

A summary of the ES6 Method Object.Assign

Redux is a powerful yet simple way of managing the state of your app, but it is a purely functional library. This is something that JavaScript developers may not have had much experience with and on its surface, sounds a bit odd. The purpose of Redux is to manage state, but one of the tenants of functional programming is you can’t alter state… so what’s up with that?

To keep things functional, Redux makes heavy use of the Object.assign method.

According to Mozilla

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.

What on earth does that mean? In a nutshell, we are making a new object from an existing object (a clone). In Redux when you need to update your state, you use a Reducer function where you pass in your current state and an object that has the new state or changes you want to make to the state. For example:

function reducer(state = initialState, action) {
  switch (action.type) {
    case UPDATE_DATA_ACTION:
    return Object.assign({}, state, {
        data: action.data
      })
    default:
      return state
  }
}

In this example, we have the function reducer that takes two properties state and action. The state is our current Redux store while action is an object that contains a key type (which is just a string var) and an object data that contains the new changes to our state.

Inside this function you can see we are using Object.assign and passing in 3 parameters, {}, state, and { data: action.data }.

Before we go into what it’s doing, lets talk about why this is needed. For that, we will take a look at a few scenarios.

Scenario #1 - Merging: a{} + b{} = ?

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

Let’s say we have two objects like those shown above and we want to merge them together so all our properties are on the same object. If we aren’t concerned with mutating an existing variable, we could do something like the following:

a.bar = b.bar;
console.log(a) // {foo: 1, bar: 2}

If you have a lot of keys, that method really isn’t very useful nor functional (as we are modifying state). For most situations you would create a function to loop through your source keys and copy them to your target. This is what Object.assign is doing under the hood in fact.

Here is an example implementation of an Object.assign polyfill:

// Object.assign function
 Object.assign = function(target) {
    'use strict';
    if (target == null) {
      throw new TypeError('Cannot convert undefined or null to object');
    }

    // Create a new empty Object
    target = Object(target);

    // Loop through the objects passed in
    for (var index = 1; index < arguments.length; index++) {
      var source = arguments[index];
      if (source != null) {
        // loop over all the keys in source object
        for (var key in source) {
          if (Object.prototype.hasOwnProperty.call(source, key)) {
            // assign the value to an identical key in our new target object.
            target[key] = source[key];
          }
        }
      }
    }
    return target;
}

Lets do that same example using Object assign:

Object.assign(a, b);
console.log(a); // Object{foo: 1, bar: 2}

Scenario #2 - Creating a new variable: var b = a{}?

The above example works and all, but we are still modifying a. In functional frameworks like Redux you should treat the input as immutable. You never modify the original state, but instead always return a new state with modifications. Object.assign makes this easy:

var c = Object.assign({}, a, b)
console.log(a) // Object {foo: 1}
console.log(b) // Object {bar: 2}
console.log(c) // Object {foo: 1, bar: 2}

What happened there? By passing an empty object as the first parameter to Object.assign we are telling it to create a new object to merge our other objects into. We can also pass in literal objects as well.

var c = Object.assign({}, a, b, {baz: 3});
console.log(c); //Object {foo: 1, bar: 2, baz: 3}
Object.assign(c, {foo: 42});
console.log(c); //Object {foo: 42, bar: 2, baz: 3}

As you can see, you can also override specific keys in an object by passing in an object with the key:newValue.

But, there is a caveat to Object.assign: it’s only making shallow copies.

Lets take a look at an example to illustrate.

var a = {foo: 1, bar: {baz: 99}};
var b = Object.assign({}, a});
b.bar.baz = 42; // b {foo:1, bar: {baz: 42}}
console.log(a); // a {foo:1, bar: {baz: 42}}

The new object we made contained another object (bar). Because Object.assign only made a shallow copy, only a reference to bar was copied to our new b variable. Because of that, any changes made to one, are made to the other, since they are both still pointing at the same object.

We need to add a step to make b a deep copy.

...
Object.assign(b.bar, a.bar)

Now we have a new copy of object a with no references. If you have many such deeply nested objects, you will want to write a function to do that.

Back to Redux

Knowing this, if you look back at our Redux reducer, you can see we are creating a new object {}, then passing in our current state and finally the state object {data: action.data} that we want to merge in. The final result is a new state that contains all the properties of the original but with the updated values (and possibly new properties) of the object we passed in.

Summary

Object.assign is a great shorthand method for making shallow copies of objects with a simple syntax. Currently you will want to have a polyfill for it in your project as there is no native support for IE. If you’re using something like Babel, this will be taken care of for you automatically.