Skip to the code
cf.

Deep freeze

Object.freeze is shallow: it freezes an object's own properties, but the objects stored in them can still change. Write deepFreeze(object), which freezes object and every object, array, and function reachable through its properties, and returns object.

Reach every own property, including symbol keys and non-enumerable ones. Don't call getters: freeze only what data properties hold. Handle cycles, and objects whose top level was frozen already (their children may not be). Primitives come back unchanged.

const config = deepFreeze({ api: { url: "/v1" }, flags: ["beta"] });
 
Object.isFrozen(config.api);   // true
Object.isFrozen(config.flags); // true
config.api.url = "/v2";        // ignored, or a TypeError in strict mode

What the tests check

  1. freezes the object and every nested object, and returns it
    const o = { a: { b: { c: 1 } } };
    expect(deepFreeze(o)).toBe(o);
    expect(Object.isFrozen(o)).toBe(true);
    expect(Object.isFrozen(o.a)).toBe(true);
    expect(Object.isFrozen(o.a.b)).toBe(true);
    
  2. freezes arrays and what they hold
    const o = deepFreeze({ list: [{ x: 1 }, [2]] });
    expect(Object.isFrozen(o.list)).toBe(true);
    expect(Object.isFrozen(o.list[0])).toBe(true);
    expect(Object.isFrozen(o.list[1])).toBe(true);
    
  3. makes nested writes throw in strict mode
    const o = deepFreeze({ a: { b: 1 } });
    expect(() => {
      "use strict";
      o.a.b = 2;
    }).toThrow(TypeError);
    expect(o.a.b).toBe(1);
    
  4. handles cycles
    const o = { name: "root", child: {} };
    o.self = o;
    o.child.parent = o;
    expect(deepFreeze(o)).toBe(o);
    expect(Object.isFrozen(o)).toBe(true);
    expect(Object.isFrozen(o.child)).toBe(true);
    
  5. reaches inside an object whose top level was already frozen
    const o = Object.freeze({ inner: { x: 1 } });
    deepFreeze(o);
    expect(Object.isFrozen(o.inner)).toBe(true);
    
  6. freezes symbol-keyed and non-enumerable properties
    const key = Symbol("key");
    const o = { [key]: { x: 1 } };
    Object.defineProperty(o, "hidden", { value: { y: 2 }, enumerable: false });
    deepFreeze(o);
    expect(Object.isFrozen(o[key])).toBe(true);
    expect(Object.isFrozen(o.hidden)).toBe(true);
    
  7. freezes functions and the objects they hold
    const fn = () => {};
    fn.options = { retries: 3 };
    deepFreeze({ fn });
    expect(Object.isFrozen(fn)).toBe(true);
    expect(Object.isFrozen(fn.options)).toBe(true);
    
  8. doesn't call getters
    const getter = spy(() => ({ fresh: true }));
    const o = {};
    Object.defineProperty(o, "lazy", { get: getter, enumerable: true });
    deepFreeze(o);
    expect(getter).not.toHaveBeenCalled();
    expect(Object.isFrozen(o)).toBe(true);
    
  9. returns primitives unchanged
    expect(deepFreeze(42)).toBe(42);
    expect(deepFreeze("text")).toBe("text");
    expect(deepFreeze(null)).toBeNull();
    expect(deepFreeze(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).