Why not to use Object.freeze for immutable JS objects?


Hello folks, Erik Marks from MetaMask here.

@danfinlay and I were recently talking about how to achieve object immutability in JS. He recalled discussing the matter with Agoric, and that Agoric uses something other than Object.freeze internally.

I am trying to understand the limitations of Object.freeze, and alternatives to it. If anyone at Agoric or otherwise has any insight, I’d greatly appreciate it.

Per the MDN article it seems to me that freezing small - whatever that means - objects recursively with Object.freeze ought to be fine in many cases. I suppose Immutable.js exists when the objects to be frozen are neither simple nor small. However, that library can significantly impact performance, in addition to having an API that tends to propagate throughout your code.



Freezing only freezes properties. JavaScript objects can hold mutable state that is unaffected by freeze. The harden function from https://github.com/Agoric/harden freezes everything it can find by transitive own property traversal[*]. If harden(obj) returns successfully, then all the object reachable from obj by own property traversal and inheritance are frozen. Usually, the most useful way to think about harden(obj) is as a way to make API surface tamper proof, rather than as a way to achieve immutability, aka, purity.

let x = 3;
const count = harden(function() => { return x++; });

The count function above is hardened but not pure.

However, if the subgraph reached from harden(obj) has no hidden mutable state, such that after harden(obj) successfully returns we know obj is pure, then before harden is called, we say that obj is purifiable.

In the default configuration of SES, all the intrinsic objects (i.e., the primordials, the objects that must exist before user code starts running) are pure. If two subgraphs share pure objects but are otherwise isolated, they cannot overtly communicate. By default, SES also denies them the special powers they need to read side channels and covert channels, like the ability to sense the duration of time. In that case, these subgraphs simply cannot communicate.

A pure module is one whose exports are pure values. A statically pure module is one that passes our static checking rules that ensure a module is pure under the assumption that it is executing under the SES default configuration and that it imports only pure values. A module that is not a pure module is a resource module. A module that is not statically pure must conservatively be assumed to be a resource module.

SES will provide a pure loader, to be shared by default by all code within the same SES root realm. The pure loader will only load statically pure modules. It will resolve all their imports only to statically pure modules, thereby ensuring that these modules only import and export pure values.

Jessie only supports pure modules. SES will also support per-compartment resource loaders that load resource modules. These resource loader can be configured to delegate to each other, and to their shared pure loader, to arrange for least-authority wiring of resource modules, as explained in the safe-modules repository.

[*] Technically, transitive reflective own property traversal. For data properties, this remains as expected — harden recurs on the property’s value. For accessor properties, harden does not call the getter. Rather, harden recurs on the getter and setter function themselves. Again, see the harden repository for details.

For reference, see