Categories
JavaScript

Avoiding Shared Mutable State in JavaScript by Deep Copying Data

Spread the love

In JavaScript, like any other program languages, stores things in variables, which can be changed on the fly. This may be a problem because we may change things accidentally that is shared. Having lots of code share the same mutable state is hard to trace. It makes debugging and read the code hard.

For example, if we change the same array in different function as follows:

let arr = [];
const foo = () => {
  arr = [1, 2, 3];
}

const bar = () => {
  arr = [4, 5, 6];
}

Then the value of arr changes depending on whether foo or bar is called last. If foo is called then arr is [1, 2, 3] . On the other hand, if bar is called then arr is [4, 5, 6] .

This is a problem because as the code gets more complex, then the more function calls there are. If lots of functions are doing things like this, then tracing the value is hard and debugging is confusing.

Also, it’s hard to read how the logic flows as functions are called with these side effects.

In this article, we look at how to do a deep copy of data to prevent mutation of shared state in a program by making deep copies of data. Also, we look at ways to prevent the mutation of data exposed from class methods.

Deep Copy

Nested Spreading

We can use the spread operator in each level of an object to do deep copying manually.

For example, given that we have the following object:

const obj = {
  foo: {
    bar: 1,
    baz: 2
  },
  a: 3
}

We can make a deep copy of it as follows:

const objCopy = {
  foo: {
    ...obj.foo
  },
  a: obj.a
};

As we can see, this is going to be a problem when we have more levels and properties. However, we do get a deep copy of the original object.

Deep Copy Via JSON.stringify and JSON.parse

We can call JSON.stringify to return a string of an object and then call JSON.parse on the stringify to return it to the original form.

This works for all properties and values that are supported by JSON, which means that entities like Symbols and functions are excluded.

For example, we can write:

const obj = {
  foo: {
    bar: 1,
    baz: 2
  },
  a: 3
}

const objCopy = JSON.parse(JSON.stringify(obj));

objCopy will be a deep copy of obj . This is because a string if immutable and JSON.parse returns a new parsed copy of the stringified object.

Copying an Instance of a Class

We can copy an instance of a class easier than with objects. We can write a clone method that returns the instance of the object to do this.

For example, we can write the following code to make one class that inherits from another class:

class Person {
  constructor(name) {
    this.name = name;
  }
}

class Employee extends Person {
  constructor(name, employeeCode) {
    super(name);
    this.employeeCode = employeeCode;
  }

  clone() {
    return new Employee(this.name, this.employeeCode);
  }
}

Then we call clone to create a new duplicate object:

const employee = new Employee('Joe', 1);
const employeeClone = employee.clone();

console.log(employee.__proto__);
console.log(employeeClone.__proto__);

In the clone method of the Employee class, we return a new instance of an Employee .

Then in the first 2 console.log outputs, we see that both employee and employeeClone have the same prototype.

Why Does Copying Help Prevent Mutating Shared State?

Copying prevents the mutation of a shared state because we copied the shared data before we attempt to change it. This is handy because it stops us from accidentally making changes to shared data.

If we don’t make changes to shared data, then tracing it is easy. Then we won’t have to worry about accidentally having different parts of our code mutating the same state.

Copying Exposed Internal Class Data Before Making Changes

Before making changes to internal class data, we should make a copy of it and then return it in a method. This prevents us from changing the class data directly.

For example, if we have the following class:

class NumArray {
  constructor() {
    this._arr = [];
  }

  add(num) {
    this._arr.push(num);
  }

  getParts() {
    return this._arr;
  }

  toString() {
    return this._arr.join('');
  }
}

and we call the methods in various ways by writing the following:

const numArr = new NumArray();
numArr.add(1);
numArr.add(2);
console.log(numArr.toString());
numArr.getParts().length = 0;
console.log(numArr.toString());

We can see that the first console.log has the value '1,2' , and the second one logs an empty string.

This is because we changed the length property of the array exposed by the getParts methods. We set it to 0, so the array is emptied.

To prevent this, we can copy the array before returning it in getParts . We write the following instead:

class NumArray {
  constructor() {
    this._arr = [];
  }

  add(num) {
    this._arr.push(num);
  }

  getParts() {
    return [...this._arr];
  }

  toString() {
    return this._arr.join(',');
  }
}

Then when we make the same calls to the NumArray class’ methods as follows:

const numArr = new NumArray();
numArr.add(1);
numArr.add(2);
console.log(numArr.toString());
numArr.getParts().length = 0;
console.log(numArr.toString());

We get '1,2' in both console.log outputs.

We can do a deep copy of data in various ways. First we can manually copy data by repeatedly using the spread operator in every level.

Also, we can use JSON.stringify and JSON.parse to copy data that can be included in JSON.

For class instances, we can make a method that returns instances of classes by return a new instance of the data with the same data. This preserves the data and the inheritance structure.

Finally, we can prevent data exposed in class methods from being modified by making a copy of it before return it in the method.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *