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]]
false
s, 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:
- Prevents modification of its properties;
- Prevents the removal of its properties; and:
- 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:
- Prevents the removal of its properties; and:
- 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.
The Monkey patch
map.set = ()=> undefined;
followed byObject.freeze(map);
unresolved?– Augusto Vasques
@Augustovasques, the
Object.freeze
in themap
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 viaMap
.– Luiz Felipe
Yes @Luizfelipe, but if you do
map.set = ()=> undefined;
he stops inserting data intomap
and Object.Freeze(map); prevents restoration from becoming read only.– Augusto Vasques
Oh yes, just right.
– Luiz Felipe
@Augustewhat idea I had in mind, I just hadn’t thought about
Object.freeze(map)
. Good addition! D– Cmte Cardeal
@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.– Augusto Vasques
@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.
– Augusto Vasques
Send the code, @Augustovasques, I was curious!
– Luiz Felipe
@Luizfelipe https://replit.com/@Ronaldovasques/Testesjs#index.js
– Augusto Vasques
https://chat.stackexchange.com/rooms/124489/https-pt-stackoverflow-com-q-509804-69296, @Augustovasques
– Luiz Felipe