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

8

4

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
Shikoku answered 31/5, 2019 at 22:22 Comment(2)
Have you tried to implement that (or find an implementation)? What happened?Microwatt
I tried something with getter and setter but it doesn't scaleShikoku
C
1

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);
Church answered 31/5, 2019 at 22:52 Comment(0)
C
2

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 :)

Claritaclarity answered 3/6, 2019 at 17:13 Comment(1)
Please, be aware that this solution will cause Maximum call stack size exceeded error if invoked on an object with circular references. Guess even MDN is not perfect...Heterozygote
C
2

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))
Covenantor answered 22/10, 2019 at 1:50 Comment(1)
Shouldn't frozen be a set rather than array? Otherwise every frozen.includes(x) will take linear time instead of constant time. That's not "slightly" slower, that's linear complexity vs quadratic complexity =DHeterozygote
P
1

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;
};
Polygnotus answered 31/5, 2019 at 22:26 Comment(5)
No, don't use this, this is an example of everything that's wrong with nodejs ecosystem. An entire package even with a test, to save you from writing the simplest recursive for loop ever with two conditions.Brita
i agree. i try to minimize external packages.Shikoku
but why to reinvent the wheel, if there is something already done, just build on top of it.Polygnotus
I mean I'll copy paste this. but not be tied to external dependency that can be changed anytime.Shikoku
Thank you for the check against null !! Since typeof ( null ) results in an object, that threw up every other recursion presented on this topic.Phytobiology
D
1

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.

Dulles answered 31/5, 2019 at 22:46 Comment(0)
C
1

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);
Church answered 31/5, 2019 at 22:52 Comment(0)
B
0

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`)
    }
  })
}
Brightwork answered 16/3, 2023 at 1:38 Comment(0)
P
0

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.

Podvin answered 3/5, 2023 at 13:51 Comment(0)
H
0

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.

Heterozygote answered 1/5 at 10:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.