Is it possible to apply or simulate immutability to an "object" of the Map type in Javascript?

Asked

Viewed 89 times

3

For objects, we can leave it unchanged, so trying to re-assign a new value to a property, adding new properties, or removing properties is not possible. Of course, in case the object suffered Object.freeze.

Let’s take the example:

var obj = { foo: 'baz' };

Object.freeze(obj);

obj.foo = null;

delete obj.foo;

obj.bar = 10;

console.log(obj); // Não mudou

Or, also, for new properties, we can define them in an immutable way through the Object.defineProperty passing some parameters to get there:

var obj = { foo: 'baz'};

Object.defineProperty(obj, 'key', {
  enumerable: false,   // não enumerável
  configurable: false, // não configurável
  writable: false,     // não gravável
  value: 'static'
});

obj.key = 100;

console.log(obj.key); // 'static'

I can even prevent an object from being extended using Object.preventExtensions:

var obj = { a: null };

Object.preventExtensions(obj);

obj.b = true;

console.log(obj);

// lança um erro
Object.defineProperty(obj, 'foo', {
  configurable: false,
  writable: false,
  value: 'true',
});

My curiosity arose regarding Map. Although this is "similar" to objects, it has demeanor a little different, what differentiates from objects. I wondered if it is possible to apply immutability to instances of Map, in the same way that can be done with objects. Obviously, the above attempts will have no effect on the Map, because they are object methods:

var map = new Map();

map.set('foo', 'baz');

// nem adiciona, como esperado
Object.defineProperty(map, 'footest', { configurable: false, writable: false });

// não fica frio
Object.freeze(map);

// não impede novas adições
Object.preventExtensions(map);

console.log(map);

map.set('baz', null);
map.set('foo', true);

console.log(map.get('baz'));
console.log(map.get('foo'));

// remove sem problema
map.delete('baz');

console.log(map.get('baz'));

  • Is there any way to work with immutability with Maps?
  • Is there any way to stop a Map to be extended when it reaches a size, for example, size equal to 3?
  • We can leave some key to a Map immutable?
  • If not, it is at least possible to create and simulate something like a immutable Map?

This question of mine is only a proof of concept. I see no use for it, but I think it’s worth knowing.

  • 2

    The Monkey patch map.set = ()=> undefined; followed by Object.freeze(map); unresolved?

  • 2

    @Augustovasques, the Object.freeze in the map has no effect on what is actually being stored on the map. But the Monkey patch, in fact, it is a solution for an already instantiated map via Map.

  • 2

    Yes @Luizfelipe, but if you do map.set = ()=> undefined; he stops inserting data into map and Object.Freeze(map); prevents restoration from becoming read only.

  • Oh yes, just right.

  • 1

    @Augustewhat idea I had in mind, I just hadn’t thought about Object.freeze(map). Good addition! D

  • 2

    @Cmtecardeal, but also does not escape the manipulation of the prototype as Luiz Felipe said. If you do Map.prototype.set.call(map,'foo','123'); the change is made.

  • @Luizfelipe, I managed with a proxy to make a map ready-only, but it causes so many side effects, some still unknown to me, that becomes an impractical activity.

  • 1

    Send the code, @Augustovasques, I was curious!

  • 1

    @Luizfelipe https://replit.com/@Ronaldovasques/Testesjs#index.js

  • 1

    https://chat.stackexchange.com/rooms/124489/https-pt-stackoverflow-com-q-509804-69296, @Augustovasques

Show 5 more comments

1 answer

5


TL;DR

It is impossible to make an instance of Map unchanging in Javascript.


First of all, a few things need to be clarified.

All the mechanisms used in the question to make an object "immutable" are only valid for objects, so we can define immutable properties:

  • Object.defineProperty allows the definition of a property with custom attributes. So, you can define a property with [[Writable]] and [[Configurable]] falses, making it immutable.

Or modify the behavior of an existing object:

  • Object.freeze, the suprassum of the three essentially makes it an unchanging object, so that:
    1. Prevents modification of its properties;
    2. Prevents the removal of its properties; and:
    3. Prevents the addition of new properties.
  • Object.seal, similar to the Object.freeze, but a little less strict, as it still allows modification of existing properties. So:
    1. Prevents the removal of its properties; and:
    2. Prevents the addition of new properties.
  • Object.preventExtensions, that only prevents the addition of new properties. It is the weakest of the three.

But note that these four mechanisms work exclusively on objects, acting on the property descriptors. Only the addition of new properties that have nothing to do with descriptors, but with the internal property [[Extensible]], which is part of an object (and not its properties, such as the property attributes).


Thus, these four mechanisms are not expected to work for immutabilize instances of Map.

To better understand, let’s compare the shapes as objects and maps (instances of Map) store their values.

Objects Instances of Map
Store their values, qualified by a key, as a property of the object itself. Store their values, qualified by a key, as a "member" of the internal "warehouse" of each Map.

Therefore, unlike objects, whose stored values can be accessed by the programmer (through Javascript’s own Apis), map values are kept in a protected "warehouse" of the programmer. You can learn more about this Internal store of the maps in section § 23.1, about Map, in the language specification. It is very enlightening.

This means that, from the Javascript Apis, it is impossible make, in fact, some member of an immutable map.


What could be done is to create a "subclass"1 of Map calling for ImmutableMap which, by overriding the method set, prevents any kind of change in values. Moreover, values can only be added in the construction.

class ImmutableMap extends Map {
  constructor(initialEntries) {
    super();
    for (const [key, val] of initialEntries) {
      super.set(key, val);
    }
  }
  
  // Sobrescreve o método `set`:
  set() {
    console.log('This map is readonly.');
    // Não vou lançar erro para fins didáticos, mas seria de bom tom:
    // throw new TypeError('Attempted to mutate a readonly map.');
  }
}

const imap = new ImmutableMap([
  ['name', 'Luiz Felipe'],
  ['publicUserId', 'lffg']
]);

console.log(imap.get('name')); //=> 'Luiz Felipe'
imap.set('name', 'Luiz Felipe 2'); //=> This map is readonly.
console.log(imap.get('name')); //=> 'Luiz Felipe'

Of course, the usefulness of it really seems minimal to me, but that’s the idea.

It is interesting to mention, too, that none of this really works to make an instance of Map (or ImmutableMap, that we implemented above) really immutable, since the can use the method Map.prototype.set with this the instance is modified. To do this, you can use Function.prototype.call or Function.prototype.apply.

The last three lines of this example demonstrate this. Please check:

class ImmutableMap extends Map {
  constructor(initialEntries) {
    super();
    for (const [key, val] of initialEntries) {
      super.set(key, val);
    }
  }
  
  // Sobrescreve o método `set`:
  set() {
    console.log('This map is readonly.');
    // Não vou lançar erro para fins didáticos, mas seria de bom tom:
    // throw new TypeError('Attempted to mutate a readonly map.');
  }
}

const imap = new ImmutableMap([
  ['name', 'Luiz Felipe'],
  ['publicUserId', 'lffg']
]);

console.log(imap.get('name')); //=> 'Luiz Felipe'
imap.set('name', 'Luiz Felipe 2'); //=> This map is readonly.
console.log(imap.get('name')); //=> 'Luiz Felipe'

// CONTRA ISTO NÃO HÁ ESCAPATÓRIA.
Map.prototype.set.call(imap, 'name', 'Luiz Felipe 3');
console.log(imap.get('name')); //=> 'Luiz Felipe 3'

Nor the Monkey patch, suggested in the comments, would resolve this limitation. It is a limitation that is due to the fact that we do not have access to store of the map.

Ah! The prototypical nature of Javascript disturbing us. But who cares, right? :-)

In short, it is valid to conclude that it is impossible to make a map immutable in Javascript.


The only "solution" I see for this is to create a completely different implementation, which uses a Map internally as a detail of implementation. But in this case, to be fair, nor is it more of an instance of Map. It’s a completely different object. So I don’t think it works as an answer. Anyway, the idea is there.


Footer

Note 1: I put in quotes because the orientation to Javascript objects is not classical, but yes prototypical.

  • 1

    The pitch of the class that extends Map was brilliant. What I had in mind was what Augusto had said. Thank you for giving me another idea ;)

Browser other questions tagged

You are not signed in. Login or sign up in order to post.