Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

9 Protecting objects from being changed



In this chapter, we’ll look at how objects can be protected from being changed. Examples include: Preventing properties being added and preventing properties being changed.

  Required knowledge: property attributes

For this chapter, you should be familiar with property attributes. If you aren’t, check out §8 “Property attributes: an introduction”.

9.1 Levels of protection: preventing extensions, sealing, freezing

JavaScript has three levels of protecting objects:

9.2 Preventing extensions of objects

Object.preventExtensions<T>(obj: T): T

This method works as follows:

Let’s use Object.preventExtensions() in an example:

const obj = { first: 'Jane' };
Object.preventExtensions(obj);
assert.throws(
  () => obj.last = 'Doe',
  /^TypeError: Cannot add property last, object is not extensible$/);

We can still delete properties, though:

assert.deepEquals(
  Object.keys(obj), ['first']);
delete obj.first;
assert.deepEquals(
  Object.keys(obj), []);

9.2.1 Checking whether an object is extensible

Object.isExtensible(obj: any): boolean

Checks whether obj is extensible – for example:

> const obj = {};
> Object.isExtensible(obj)
true
> Object.preventExtensions(obj)
{}
> Object.isExtensible(obj)
false

9.3 Sealing objects

Object.seal<T>(obj: T): T

Description of this method:

The following example demonstrates that sealing makes the object non-extensible and its properties unconfigurable.

const obj = {
  first: 'Jane',
  last: 'Doe',
};

// Before sealing
assert.equal(Object.isExtensible(obj), true);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    first: {
      value: 'Jane',
      writable: true,
      enumerable: true,
      configurable: true
    },
    last: {
      value: 'Doe',
      writable: true,
      enumerable: true,
      configurable: true
    }
  });

Object.seal(obj);

// After sealing
assert.equal(Object.isExtensible(obj), false);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    first: {
      value: 'Jane',
      writable: true,
      enumerable: true,
      configurable: false
    },
    last: {
      value: 'Doe',
      writable: true,
      enumerable: true,
      configurable: false
    }
  });

We can still change the the value of property .first:

obj.first = 'John';
assert.deepEqual(
  obj, {first: 'John', last: 'Doe'});

But we can’t change its attributes:

assert.throws(
  () => Object.defineProperty(obj, 'first', { enumerable: false }),
  /^TypeError: Cannot redefine property: first$/);

9.3.1 Checking whether an object is sealed

Object.isSealed(obj: any): boolean

Checks whether obj is sealed – for example:

> const obj = {};
> Object.isSealed(obj)
false
> Object.seal(obj)
{}
> Object.isSealed(obj)
true

9.4 Freezing objects

Object.freeze<T>(obj: T): T;
const point = { x: 17, y: -5 };
Object.freeze(point);

assert.throws(
  () => point.x = 2,
  /^TypeError: Cannot assign to read only property 'x'/);

assert.throws(
  () => Object.defineProperty(point, 'x', {enumerable: false}),
  /^TypeError: Cannot redefine property: x$/);

assert.throws(
  () => point.z = 4,
  /^TypeError: Cannot add property z, object is not extensible$/);

9.4.1 Checking whether an object is frozen

Object.isFrozen(obj: any): boolean

Checks whether obj is frozen – for example:

> const point = { x: 17, y: -5 };
> Object.isFrozen(point)
false
> Object.freeze(point)
{ x: 17, y: -5 }
> Object.isFrozen(point)
true

9.4.2 Freezing is shallow

Object.freeze(obj) only freezes obj and its properties. It does not freeze the values of those properties – for example:

const teacher = {
  name: 'Edna Krabappel',
  students: ['Bart'],
};
Object.freeze(teacher);

// We can’t change own properties:
assert.throws(
  () => teacher.name = 'Elizabeth Hoover',
  /^TypeError: Cannot assign to read only property 'name'/);

// Alas, we can still change values of own properties:
teacher.students.push('Lisa');
assert.deepEqual(
  teacher, {
    name: 'Edna Krabappel',
    students: ['Bart', 'Lisa'],
  });

9.4.3 Implementing deep freezing

If we want deep freezing, we need to implement it ourselves:

function deepFreeze(value) {
  if (Array.isArray(value)) {
    for (const element of value) {
      deepFreeze(element);
    }
    Object.freeze(value);
  } else if (typeof value === 'object' && value !== null) {
    for (const v of Object.values(value)) {
      deepFreeze(v);
    }
    Object.freeze(value);
  } else {
    // Nothing to do: primitive values are already immutable
  } 
  return value;
}

Revisiting the example from the previous section, we can check if deepFreeze() really freezes deeply:

const teacher = {
  name: 'Edna Krabappel',
  students: ['Bart'],
};
deepFreeze(teacher);

assert.throws(
  () => teacher.students.push('Lisa'),
  /^TypeError: Cannot add property 1, object is not extensible$/);

9.5 Further reading