Muhammad Umer
Muhammad Umer

Reputation: 18097

Is there an easy way to completely freeze object and its children in Javascript, (Deep Freeze)?

Often you have to pass an object as parameter. Functions accessing those objects are pass by reference and can change original object. Which depending on situation can be unwanted outcome. So is there a way to freeze object. I know about Object.freeze()

But it doesn't affect objects/arrays inside it.

For example

a = { arr: [1,2,3]}; 
Object.freeze(a);
a.arr.push(4); // still works

Upvotes: 4

Views: 2101

Answers (8)

Klesun
Klesun

Reputation: 13673

My 5 cents, since none of the answers here or in the duplicate question would provide a solution that would be both correct and efficient at same time.

Based on the MDN documentation of Object.freeze(), but modified to account for circular references.

function deepFreeze(object) {
  const occurrences = new WeakSet();

  function deepFreezeCircularlySafe(object) {
    if (occurrences.has(object)) {
      return object;
    }
    occurrences.add(object);

    // Retrieve the property names defined on object
    const propNames = Reflect.ownKeys(object);

    // Freeze properties before freezing self
    for (const name of propNames) {
      const value = object[name];

      if ((value && typeof value === "object") || typeof value === "function") {
        deepFreezeCircularlySafe(value);
      }
    }

    return Object.freeze(object);
  }

  return deepFreezeCircularlySafe(object);
}

The Object.isFrozen() solution that many promote will actually give false positives on input that already had some of the nodes frozen before the call to deepFreeze() and therefore is not safe to use as it does not guarantee that all of the sub-nodes would really be frozen.

Upvotes: 0

Muhammad Usman
Muhammad Usman

Reputation: 478

Solution from the MDN documentation of Object.freeze():

function deepFreeze(object) {
  // Retrieve the property names defined on object
  const propNames = Reflect.ownKeys(object);

  // Freeze properties before freezing self  
  for (const name of propNames) {
    const value = object[name];

    if ((value && typeof value === "object") || typeof value === "function") {
      deepFreeze(value);
    }
  }

  return Object.freeze(object);
}

let a = { arr: [1,2,3]}; 
deepFreeze(a);
a.arr.push(4); // TypeError: Cannot add property 3, object is not extensible

Thank you :)

Upvotes: 2

Ameen Demidem
Ameen Demidem

Reputation: 19

You might want to consider immer. It's an excellent tool to create immutable Js data structures.

In your case, that would be:

const a = produce({ arr: [1,2,3] }, () => {})

The () => {} is necessary because produce expects a function that mutates the first param, but there is no mutation required here.

If you are trying to freeze a couple of data structures and won't use immutability otherwise, it might not be worth the cost of an extra third-party library. And the above solutions will be enough.

Upvotes: -1

kofifus
kofifus

Reputation: 19295

Another approach is to use a Proxy. Instead of deep freezing the object you get a proxy to the object that forbids writing:

const immutableProxy = o => {
  if (o===null || typeof o !== 'object') return o
  return new Proxy(o, {
    get(obj, prop) {
      return immutableProxy(obj[prop])
    },
    set(obj, prop) {
      throw new Error(`Can not set prop ${prop} on immutable object`)
    }
  })
}

Upvotes: 0

dsharhon
dsharhon

Reputation: 547

To deep-freeze all enumerable properties (ES2015+):

// Recursively freeze an object
const deepFreeze = x => {
  Object.freeze(x)
  Object.values(x).forEach(deepFreeze)
}

If you have circular references:

// Recursively freeze an object with circular references
const deepFreeze = x => {
  Object.freeze(x)
  Object.values(x).filter(x => !Object.isFrozen(x)).forEach(deepFreeze)
}

If you also have to deep-freeze any shallow-frozen stuff (slightly slower):

// Recursively deep freeze an object with circular references
const deepFreeze = (x, frozen = []) => {
  if (frozen.includes(x)) return null
  frozen.push(x)
  Object.freeze(x)
  Object.values(x).forEach(x => deepFreeze(x, frozen))
}

tldr; Two-liner:

const deepFreeze = (x, frozen = []) => frozen.includes(x) ||
  frozen.push(Object.freeze(x)) && Object.values(x).forEach(x => deepFreeze(x, frozen))

Upvotes: 2

Jack Bashford
Jack Bashford

Reputation: 44105

You can make a very simple recursive solution like so:

let a = {
  arr: [1, 2, 3],
  name: "A. Bc"
};

const deepFreeze = o => {
  for (let [key, value] of Object.entries(o)) {
    if (o.hasOwnProperty(key) && typeof value == "object") {
      deepFreeze(value);
    }
  }
  Object.freeze(o);
  return o;
}

deepFreeze(a);

try {
  a.arr.push(4);
} catch(e) {
  console.log("Error: ", e);
}
console.log(a);

Upvotes: 1

ibrahim tanyalcin
ibrahim tanyalcin

Reputation: 6491

If you look at MDN, there is a function there that suggests deepFreeze functionality however it is not stack safe. I personally have an ES5 version to async iterate. For ES6 something along these lines might work, I did not test it thoroughly though:

function deepFreeze(o,promises,oldestParent){
    promises = promises || [];
    oldestParent = oldestParent || o;
    promises.push(
        Promise.resolve().then(function(){
            Object.values(Object.freeze(o)).forEach(function(d,i){
                typeof d === "object" && deepFreeze(d,promises,oldestParent);
            });
            return oldestParent;
        })
    );
    return Promise.all(promises).then((a)=>a[0]);
}

var o = {a:3,b:{test:1,test2:2},c:1};
deepFreeze(o).then(function(x){console.log(x)}); //o is deep frozen

Warning: I assume your object's properties are enumerable, if not then use getOwnPropertyNames instead.

Upvotes: 1

Rami Jarrar
Rami Jarrar

Reputation: 4643

Checkout deep-freeze it does recursively Object.freeze() on objects

Here's how they implement it

function deepFreeze (o) {
  Object.freeze(o);

  Object.getOwnPropertyNames(o).forEach(function (prop) {
    if (o.hasOwnProperty(prop)
    && o[prop] !== null
    && (typeof o[prop] === "object" || typeof o[prop] === "function")
    && !Object.isFrozen(o[prop])) {
      deepFreeze(o[prop]);
    }
  });

  return o;
};

Upvotes: 1

Related Questions