Skip to the code
cf.

Deep clone

Write deepClone(value). It returns a copy of value made all the way down, so changing the copy, at any depth, never changes the original.

Copy arrays, plain objects, class instances, Map, Set, Date, and RegExp, all the way down, including Map keys and Set members. Keep each object's prototype, so class instances keep their methods, and copy every own enumerable property, symbol keys included. Primitives and functions come back as they are.

An object that appears twice in the original appears twice in the copy as one copied object, and a cycle becomes the same cycle in the copy.

const original = { when: new Date(0), tags: new Set(["a"]), list: [1, { b: 2 }] };
original.self = original;
 
const copy = deepClone(original);
copy.self === copy;                // true
copy.tags !== original.tags;       // true
copy.list[1] !== original.list[1]; // true
copy.when.getTime();               // 0

What the tests check

  1. copies nested objects and arrays
    const original = { a: [1, { b: 2 }], c: { d: "x" } };
    const copy = deepClone(original);
    expect(copy).toEqual(original);
    expect(copy).not.toBe(original);
    expect(copy.a).not.toBe(original.a);
    expect(copy.a[1]).not.toBe(original.a[1]);
    expect(copy.c).not.toBe(original.c);
    
  2. turns a cycle into the same cycle
    const original = { name: "root", list: [] };
    original.self = original;
    original.list.push(original);
    const copy = deepClone(original);
    expect(copy).not.toBe(original);
    expect(copy.self).toBe(copy);
    expect(copy.list[0]).toBe(copy);
    
  3. keeps shared references shared
    const shared = { x: 1 };
    const copy = deepClone({ a: shared, b: [shared] });
    expect(copy.a).toBe(copy.b[0]);
    expect(copy.a).not.toBe(shared);
    
  4. copies Maps, keys and values included
    const key = { id: 1 };
    const original = new Map([[key, { v: 1 }], ["plain", 2]]);
    const copy = deepClone(original);
    expect(copy).toBeInstanceOf(Map);
    expect(copy).not.toBe(original);
    const [[copiedKey, copiedValue]] = copy;
    expect(copiedKey).not.toBe(key);
    expect(copiedKey).toEqual({ id: 1 });
    expect(copiedValue).not.toBe(original.get(key));
    expect(copiedValue).toEqual({ v: 1 });
    expect(copy.get("plain")).toBe(2);
    
  5. copies Sets, keeping a member shared with a Map key
    const key = { id: 1 };
    const copy = deepClone({ m: new Map([[key, true]]), s: new Set([key, "x"]) });
    expect(copy.s).toBeInstanceOf(Set);
    const [member] = copy.s;
    expect(member).not.toBe(key);
    expect(member).toBe([...copy.m.keys()][0]);
    expect(copy.s.has("x")).toBe(true);
    
  6. copies dates
    const when = new Date(86400000);
    const copy = deepClone({ a: when, b: when });
    expect(copy.a).toBeInstanceOf(Date);
    expect(copy.a).not.toBe(when);
    expect(copy.a.getTime()).toBe(86400000);
    expect(copy.b).toBe(copy.a);
    
  7. keeps prototypes, so class instances keep their methods
    class Point {
      constructor(x) {
        this.x = x;
      }
      double() {
        return this.x * 2;
      }
    }
    const original = new Point(2);
    const copy = deepClone(original);
    expect(copy).toBeInstanceOf(Point);
    expect(copy).not.toBe(original);
    expect(copy.double()).toBe(4);
    
  8. copies regular expressions and symbol-keyed properties
    const key = Symbol("key");
    const original = { re: /a+/gi, [key]: [1] };
    const copy = deepClone(original);
    expect(copy.re).not.toBe(original.re);
    expect(String(copy.re)).toBe("/a+/gi");
    expect(copy[key]).toEqual([1]);
    expect(copy[key]).not.toBe(original[key]);
    
  9. returns primitives and functions as they are
    const fn = () => 1;
    expect(deepClone(fn)).toBe(fn);
    expect(deepClone({ fn }).fn).toBe(fn);
    expect(deepClone(null)).toBeNull();
    expect(deepClone("s")).toBe("s");
    expect(deepClone(NaN)).toBeNaN();
    expect(deepClone(undefined)).toBeUndefined();
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).