Deep cloning nested objects natively without using lodash

6 min read views

Did you know that you could easily clone deeply nested objects in JavaScript without using lodash, all just by using a single function ?

Yes, structuredClone() function lets you do just that and it's built into the Web APIs.

const currentUser = {
  full_name: {
    first_name: 'Anil',
    last_name: 'Seervi'
  },
  joined_at: new Date(0),
  languages: ['English', 'JavaScript']
};
 
const cloneUser = structuredClone(currentUser);
const currentUser = {
  full_name: {
    first_name: 'Anil',
    last_name: 'Seervi'
  },
  joined_at: new Date(0),
  languages: ['English', 'JavaScript']
};
 
const cloneUser = structuredClone(currentUser);

In the example, you can notice that nested object full_name, Date Object joined_at and the Array languages are all well preserved and cloned into cloneUser variable.

currentUser.full_name; // Object: {first_name: "Anil", last_name: "Seervi"}
currentUser.joined_at; // Date: Thu Jan 01 1970 05:30:00 GMT+0530 (India Standard Time)
currentUser.languages; // Array: ["English", "JavaScript"]
currentUser.full_name; // Object: {first_name: "Anil", last_name: "Seervi"}
currentUser.joined_at; // Date: Thu Jan 01 1970 05:30:00 GMT+0530 (India Standard Time)
currentUser.languages; // Array: ["English", "JavaScript"]

That's right, structuredClone can clone many other types of Objects. We'll discuss all the other types and also it's restrictions in the following sections. But before that let's have a look at the syntax of structuredClone.

Syntax

structuredClone(value);
structuredClone(value, options);
structuredClone(value);
structuredClone(value, options);
Parameters

value : The object to be cloned. This can be any structured-cloneable type. options(optional) : An object with the following properties: transfer : An array of transferable objects that will be moved rather than cloned to the returned object.

Return Type

The returned value is a deep copy of the original value.

Exceptions

DataCloneError DOMException

Thrown if any part of the input value is not serializable.

Structured cloneable types

const multipleTypesObject = {
  set: new Set([1, 3, 2]),
  map: new Map([[3, 2]]),
  regex: /foobar/,
  deep: { array: [{ file: new File(someBlobData, 'file.txt') }] },
  error: new Error('Hello!')
};
multipleTypesObject.circular = multipleTypesObject;
 
const fullyCloned = structuredClone(multipleTypesObject);
// ✅ All good, fully and deeply copied!
const multipleTypesObject = {
  set: new Set([1, 3, 2]),
  map: new Map([[3, 2]]),
  regex: /foobar/,
  deep: { array: [{ file: new File(someBlobData, 'file.txt') }] },
  error: new Error('Hello!')
};
multipleTypesObject.circular = multipleTypesObject;
 
const fullyCloned = structuredClone(multipleTypesObject);
// ✅ All good, fully and deeply copied!

structuredClone can not only clone Primitive types, additionally it can also:

Preserving circular references

Using structuredClone to deep copy an Object will also preserve any circular references in that object. Have a look at the example below where itself preserves it's reference to the parent Object.

// Create an object with a value and a circular reference to itself.
const user = { name: 'Anil' };
user.itself = user;
 
// Clone it
const clone = structuredClone(user);
 
console.assert(clone !== user); // the objects are not the same (not same identity)
console.assert(clone.name === 'Anil'); // they do have the same values
console.assert(clone.itself === clone); // and the circular reference is preserved
// Create an object with a value and a circular reference to itself.
const user = { name: 'Anil' };
user.itself = user;
 
// Clone it
const clone = structuredClone(user);
 
console.assert(clone !== user); // the objects are not the same (not same identity)
console.assert(clone.name === 'Anil'); // they do have the same values
console.assert(clone.itself === clone); // and the circular reference is preserved

Transferring values

Apart from deeply cloning objects, you can also transfer certain Transferable objects from original object to the cloned object, using the transfer property of the options parameter. Transferring makes the original object unusable.

// 16MB = 1024 * 1024 * 16
const uInt8Array = Uint8Array.from({ length: 1024 * 1024 * 16 }, (v, i) => i);
 
console.log(uInt8Array.byteLength); // 16777216
 
const transferred = structuredClone(uInt8Array, {
  transfer: [uInt8Array.buffer]
});
console.log(uInt8Array.byteLength); // 0, because it was buffer was transferred
// 16MB = 1024 * 1024 * 16
const uInt8Array = Uint8Array.from({ length: 1024 * 1024 * 16 }, (v, i) => i);
 
console.log(uInt8Array.byteLength); // 16777216
 
const transferred = structuredClone(uInt8Array, {
  transfer: [uInt8Array.buffer]
});
console.log(uInt8Array.byteLength); // 0, because it was buffer was transferred

Why not spread the objects?

It is important to note that spreading the object will only make a shallow copy and not a deep copy.

const currentUser = {
  full_name: {
    first_name: 'Anil',
    last_name: 'Seervi'
  },
  joined_at: new Date(0),
  languages: ['English', 'JavaScript']
};
 
const spreadObject = {
  ...currentUser,
  full_name: { ...currentUser.full_name }
};
 
// 🚩 oops - we just added "CSS" to both the copy *and* the original array
spreadObject.languages.push('CSS');
 
// 🚩 oops - we just updated the date for the copy *and* original date
spreadObject.joined_at.setTime(969);
const currentUser = {
  full_name: {
    first_name: 'Anil',
    last_name: 'Seervi'
  },
  joined_at: new Date(0),
  languages: ['English', 'JavaScript']
};
 
const spreadObject = {
  ...currentUser,
  full_name: { ...currentUser.full_name }
};
 
// 🚩 oops - we just added "CSS" to both the copy *and* the original array
spreadObject.languages.push('CSS');
 
// 🚩 oops - we just updated the date for the copy *and* original date
spreadObject.joined_at.setTime(969);

Which means that nested objects will just share the same references and updating one will also update the other.

Why not just JSON.parse(JSON.stringify(x))?

So you happen to know this trick, its fast and has gotten the job done for you, but do you also know what are its shortcomings ?

Let's take an example to understand that :

const event = {
  title: 'Pusblish new article',
  date: new Date('4/1/2023')
};
 
// 🚩 JSON.stringify converted the `date` to a string
const wrongEvent = JSON.parse(JSON.stringify(event));
 
console.log(wrongEvent);
/*
{
  title: "Publish new article",
  date: "2023-03-31T18:30:00.000Z"
}
*/
const event = {
  title: 'Pusblish new article',
  date: new Date('4/1/2023')
};
 
// 🚩 JSON.stringify converted the `date` to a string
const wrongEvent = JSON.parse(JSON.stringify(event));
 
console.log(wrongEvent);
/*
{
  title: "Publish new article",
  date: "2023-03-31T18:30:00.000Z"
}
*/

So you see in this example how date which was supposed to be a Date object was converted to a string.

There are more types of objects that JSON.parse(JSON.stringify(x)) can't convert properly. Let's use the structured cloneable types example to see what output we get.

const multipleTypesObject = {
  set: new Set([1, 3, 2]),
  map: new Map([[3, 2]]),
  regex: /foobar/,
  deep: { array: [{ file: new File(someBlobData, 'file.txt') }] },
  error: new Error('Hello!')
};
 
const totallyWrongCopy = JSON.parse(JSON.stringify(multipleTypesObject));
const multipleTypesObject = {
  set: new Set([1, 3, 2]),
  map: new Map([[3, 2]]),
  regex: /foobar/,
  deep: { array: [{ file: new File(someBlobData, 'file.txt') }] },
  error: new Error('Hello!')
};
 
const totallyWrongCopy = JSON.parse(JSON.stringify(multipleTypesObject));

If we try logging totallyWrongCopy we would get :

{
  "set": {},
  "map": {},
  "regex": {},
  "deep": {
    "array": [
      { file:{}}
    ]
  },
  "error": {},
}
{
  "set": {},
  "map": {},
  "regex": {},
  "deep": {
    "array": [
      { file:{}}
    ]
  },
  "error": {},
}

Where as using a structuredClone would have cloned everything just fine. One more thing to note is that you cannot JSON.stringify a circular object.

What can structuredClone not clone ?

// Throws DataCloneError
structuredClone({ fn: () => {} });
 
// Throws DataCloneError
structuredClone({ el: document.body });
 
structuredClone({
  get foo() {
    return 'bar';
  }
});
// Becomes: { foo: 'bar' }
 
// 🚩 Object prototypes
class MyClass {
  foo = 'bar';
  myMethod() {
    /* ... */
  }
}
const myClass = new MyClass();
 
const cloned = structuredClone(myClass);
// Becomes: { foo: 'bar' }
 
cloned instanceof myClass; // false
// Throws DataCloneError
structuredClone({ fn: () => {} });
 
// Throws DataCloneError
structuredClone({ el: document.body });
 
structuredClone({
  get foo() {
    return 'bar';
  }
});
// Becomes: { foo: 'bar' }
 
// 🚩 Object prototypes
class MyClass {
  foo = 'bar';
  myMethod() {
    /* ... */
  }
}
const myClass = new MyClass();
 
const cloned = structuredClone(myClass);
// Becomes: { foo: 'bar' }
 
cloned instanceof myClass; // false

Full list of supported types

More simply put, anything not in the below list cannot be cloned:

JS Built-ins

ArrayArrayBufferBooleanDataViewDateError types (those specifically listed below), Map , Object but only plain objects (e.g. from object literals), Primitive types, except symbol (aka numberstringnullundefinedbooleanBigInt), RegExpSetTypedArray

Error types

ErrorEvalErrorRangeErrorReferenceError , SyntaxErrorTypeErrorURIError

Web/API types

AudioDataBlobCryptoKeyDOMExceptionDOMMatrixDOMMatrixReadOnlyDOMPointDomQuadDomRectFileFileListFileSystemDirectoryHandleFileSystemFileHandleFileSystemHandleImageBitmapImageDataRTCCertificateVideoFrame

Browsers and Runtime support for structuredClone

Apart from the availability in workers, structuredClone has pretty good support in all major browsers and runtimes.

cd ..

Subscribe for updatesDiscuss on TwitterSuggest Change