BerandaComputers and TechnologyCopying Objects in JavaScript

Copying Objects in JavaScript

In this article we will look at the various ways an object can be copied in Javascript. We will take a look at both
shallow and deep copying.

Before we begin, it is worth mentioning a few basics: objects in Javascript are simply references to a location in
memory. These references are mutable, i.e. they can be reassigned. Thus, simply making a copy of a reference only
results in 2 references pointing to the same location in memory:

var foo = {
    a : "abc"
}
console.log(foo.a); // abc

var bar = foo;
console.log(bar.a); // abc

foo.a = "yo foo";
console.log(foo.a); // yo foo
console.log(bar.a); // yo foo

bar.a = "whatup bar?";
console.log(foo.a); // whatup bar?
console.log(bar.a); // whatup bar?    

As you see in the above example, both foo and bar are reflecting the change done in either object. Thus, making a copy
of an object in Javascript requires some care depending upon your use case.

Shallow copy

If your object only has properties which are value types, you can use the spread syntax or
Object.assign(...)

var obj = { foo: "foo", bar: "bar" };

var copy = { ...obj }; // Object { foo: "foo", bar: "bar" }
var obj = { foo: "foo", bar: "bar" };

var copy = Object.assign({}, obj); // Object { foo: "foo", bar: "bar" }

Note that both of the above methods can be used to copy property values from multiple source objects to a
target object:

var obj1 = { foo: "foo" };
var obj2 = { bar: "bar" };

var copySpread = { ...obj1, ...obj2 }; // Object { foo: "foo", bar: "bar" }
var copyAssign = Object.assign({}, obj1, obj2); // Object { foo: "foo", bar: "bar" }

The problem with the above methods lies in the fact that for objects with properties which are themselves objects,
only the references are copied over, i.e. it is the equivalent of doing var bar = foo; as in the first
code example:

var foo = { a: 0 , b: { c: 0 } };
var copy = { ...foo };

copy.a = 1;
copy.b.c = 2;

console.dir(foo); // { a: 0, b: { c: 2 } }
console.dir(copy); // { a: 1, b: { c: 2 } }
Deep copy (with caveats)

In order to deep copy objects, a potential solution can be to serialize the object to a string and then deserialize it
back:

var obj = { a: 0, b: { c: 0 } };
var copy = JSON.parse(JSON.stringify(obj));

Unfortunately, this method only works when the source object contains serializable value types and does not have any
circular references. An example of a non-serializable value type is the Date object – even though it is
printed in ISO format on stringification, JSON.parse only interprets it as a string and not as a
Date object.

Deep copy (with fewer caveats)

For more complex cases, one could make use of a newer HTML5 cloning algorithm called
“structured clone”. Unfortunately, at the time of writing it is still limited to certain built-in types but it supports many more types
than what JSON.parse does: Date, RegExp, Map, Set, Blob, FileList, ImageData, sparse and typed Array. It
also preserves references within the cloned data, allowing it to support cyclical and recursive structures that don’t
work with the above mentioned serialization method.

Currently, there is no direct way of calling the structured clone algorithm but there are some newer browser features
that use this algorithm under the hood. Thus, there are a couple of workarounds that could potentially be used to deep
copy objects.

Via MessageChannels: the idea behind this is to leverage the serialization algorithm used by a communication
feature. Since this feature is event based, the resultant clone is also an asynchronous operation.

class StructuredCloner {
  constructor() {
    this.pendingClones_ = new Map();
    this.nextKey_ = 0;

    const channel = new MessageChannel();
    this.inPort_ = channel.port1;
    this.outPort_ = channel.port2;

    this.outPort_.onmessage = ({data: {key, value}}) => {
      const resolve = this.pendingClones_.get(key);
      resolve(value);
      this.pendingClones_.delete(key);
    };
    this.outPort_.start();
  }

  cloneAsync(value) {
    return new Promise(resolve => {
      const key = this.nextKey_++;
      this.pendingClones_.set(key, resolve);
      this.inPort_.postMessage({key, value});
    });
  }
}

const structuredCloneAsync = window.structuredCloneAsync =
    StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner);


const main = async () => {
  const original = { date: new Date(), number: Math.random() };
  original.self = original;

  const clone = await structuredCloneAsync(original);

  // different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));

  console.log("Assertions complete.");
};

main();

Via the history API: both history.pushState() and history.replaceState() create a
structured clone of their first argument! Note that while this method is synchronous, manipulating browser history is
not a fast operation and calling this method repeatedly can lead to browser unresponsiveness.

const structuredClone = obj => {
  const oldState = history.state;
  history.replaceState(obj, null);
  const clonedObj = history.state;
  history.replaceState(oldState, null);
  return clonedObj;
};

Via the
notification API
: when creating a new notification, the constructor creates a structured clone of its associated data. Note that it
also attempts to display a browser notification to the user, but this will silently fail unless the application has
requested permissions to display notifications. In the case that permission was granted, the notification is
immediately closed.

const structuredClone = obj => {
  const n = new Notification("", {data: obj, silent: true});
  n.onshow = n.close.bind(n);
  return n.data;
};
Deep copy in Node.js

As of version 8.0.0, Node.js provides a
serialization api which is compatible with
structured clone. Note that this API is marked as experimental at the time of writing:

const v8 = require('v8');
const buf = v8.serialize({a: 'foo', b: new Date()});
const cloned = v8.deserialize(buf);
cloned.b.getMonth();

For versions below 8.0.0 or for a more stable implementation, one can use lodash’s
cloneDeep method, which is also loosely based on the
structured clone algorithm.

Conclusion

To sum up, the best algorithm for copying objects in Javascript is heavily dependent on the context and type of
objects that you are looking to copy. While lodash is the safest bet for a generic deep copy function, you might get a
more efficient implementation if you roll your own, the following is an example of a simple deep clone that works for
dates as well:

function deepClone(obj) {
  var copy;

  // Handle the 3 simple types, and null or undefined
  if (null == obj || "object" != typeof obj) return obj;

  // Handle Date
  if (obj instanceof Date) {
    copy = new Date();
    copy.setTime(obj.getTime());
    return copy;
  }

  // Handle Array
  if (obj instanceof Array) {
    copy = [];
    for (var i = 0, len = obj.length; i < len; i++) {
        copy[i] = deepClone(obj[i]);
    }
    return copy;
  }

  // Handle Function
  if (obj instanceof Function) {
    copy = function() {
      return obj.apply(this, arguments);
    }
    return copy;
  }

  // Handle Object
  if (obj instanceof Object) {
      copy = {};
      for (var attr in obj) {
          if (obj.hasOwnProperty(attr)) copy[attr] = deepClone(obj[attr]);
      }
      return copy;
  }

  throw new Error("Unable to copy obj as type isn't supported " + obj.constructor.name);
}

Personally, I'm looking forward to be able to use structured clone everywhere and finally put this issue to rest,
happy cloning 🙂

HackerNews submission / discussion

Read More

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments