immutable objects

As we known, we can use Object.freeze(), Object.seal(), and Object.preventExtensions() to try to make JavaScript objects immutable. But maybe it is not so convenient that we can control certain items, like only preventing from removing properties:

Add Properties Remove Properties Modify Properties
Object.preventExtensions ×
Object.seal × ×
Object.freeze × × ×

What is your question?

Why do not JavaScript exports a method with a parameter to specify what level we intend to freeze? For example, 1 << 0 for add, 1 << 1 for delete, 1 << 2 for modify, 1 << 3 for deeply and Object.freeze(1 << 4 - 1) means to prevent from adding, deleting, and modifying properties depply.

Why e separate methods rather than one method that took an argument? Maybe someone will speak to the motivation; for me, I’d find a single bitwise argument to be incredibly confusing.

bitwise is just designed for multiple values in one parameter, and you can also split them into multiple parameters instead, like Object.freeze([add [, delete [, modify [, depply]]]]. In my opinion, separated method definitions make it harder to remember for developers.

Is there a use case for having a different combination of these setting then what those functions provide? For example, It seems unatural to have an object where you you can add and delete it's members, but you can't modify them.

1 Like

For me, the definition of a method should be so elastic that developers can easily use it in any cases event thouth it is not natural sometimes. Perhaps, everyone has its own choices.

What's more important to me is not what features a language adds, but what restrictions a language imposes. A language with a set of restrictions that developers are familiar with will make code with more predictable behavior that's easier to follow. Some concrete examples:

  • Javascript introduced let, which narrows the scope in which variables are defined to a block instead of a function. This restriction makes code easier to follow because it's easier to keep track of what variables are available at a given position in the code.
  • Javascript also introduces const, which is exactly the same as let, but with more restrictions. When someone sees "const", they can know that that variable won't be reassigned, making that reader have one less thing to keep track of when reading the code - less mental overhead.
  • Javascript introduced functions like Object.freeze() and Object.seal() to allow code writers to easily set restrictions to what can be done with an object. When a bug happens in your code, you don't have to worry about it being caused by a missing member on a sealed object, etc.

While these restrictions are great, implementing them came at a cost, because javascript wasn't originally designed with them in mind. Take this slightly contrived example:

function sendQuery(params) {
  const timeout = params.timeout
  delete params.timeout
  request('POST /api/some/resource', params, timeout)
}

At a glance, this function seems harmless, but if you happened to have provided a sealed object to it, then it will not behave as intended. Despite the potential oddities that arise from freezing and sealing objects, the language authors decided that the restrictions it provided outweigh the oddities it creates (and the added complexity it adds to understanding object behavior).

Allowing users to create their own custom sets of object modification rules without a clear reason to do so seems like a step backward. Such a feature imposes fewer restrictions on how an object can behave, making object behavior even more difficult to understand. We're just giving us all of the cons of such decisions in the hopes that someone, somewhere can find a pro.

I will note that these kinds of arbitrary access restrictions can be emulated with proxies though, so if someone really needed an object where properties can be added and removed but not modified, they still have the power to do so in the language. It's just not as convenient to do, which is a good thing, because that is not behavior that a code reader would expect, so it should only be done in the rarest of cases. And if there arises a common need for such behavior, libraries can be made to make such proxying easier to do, or if it's common enough, it can make its way as an official feature into the language.

With all of that said - these points are all just my opinion, and if you don't agree, that's great! It's good for there to be a variety of points of view out there. I am, after all, just some guy on the internet raddling of his own opinions.

1 Like

If the JavaScript lang hopes to create a convention, it can just define only three types of immutable patterns of a JavaScript object and as I know maybe it is the historical process resulting in the splitting definitions of three different methods. But these methods are specified under ES6 at the same time and I don't know where to find the proposal.

In this time, maybe the author can review whether these methods can be merged into one? When it comes to the restriction of immutable patterns, this is another topic.

At least, splitting three different methods, and adding three corresponding methods (Object.isFrozon, Object.isSealed, Object.isExtensible) to check three types of patterns seems redundant, and make it more hard to remember.

You do have a point that it is a little weird to have 3 different functions for different types of modification restrictions, and another 3 to check what's applied.

I personally have never had a need for Object.isExtensible - I'm just not deleting properties from objects often, so have never needed to explicitly allowing property deletion while wanting to restrict adding properties. Most of the time I'm just using Object.freeze(), and on occasion, Object.seal(). And, I agree, it can be a little difficult to remember which does what.

There may be the same problem when designing methods for handling a series of Promises:

name description
Promise.allSettled does not short-circuit until all promises are settled
Promise.all short-circuits when one promise is rejected
Promise.race short-circuits when one promise is settled
Promise.any short-circuits when one promise is resolved (fulfilled)

It should be a overhead for developers to remember them, especially when there is proposals for Promise.anySettled. In my opinion, it is time for the committee to revise such similar methods and combine them into one with different parameters.

The proposal for Promise.anySettled was rejected.

These methods will always exist, and it's a difficult argument that creating a fifth method will reduce confusion. Having to remember combinations of arguments is imo much MORE confusing than having to remember completely different method names.

1 Like

The main difficulty I think is that changes may break a lot of applications. Thanks for your answer. @ljharb