This is an old question, but I think I have a good case where freeze might help. I had this problem today.
The problem
class Node {
constructor() {
this._children = [];
this._parent = undefined;
}
get children() { return this._children; }
get parent() { return this._parent; }
set parent(newParent) {
// 1. if _parent is not undefined, remove this node from _parent's children
// 2. set _parent to newParent
// 3. if newParent is not undefined, add this node to newParent's children
}
addChild(node) { node.parent = this; }
removeChild(node) { node.parent === this && (node.parent = undefined); }
...
}
As you can see, when you change the parent, it automatically handles the connection between these nodes, keeping children and parent in sync. However, there is one problem here:
let newNode = new Node();
myNode.children.push(newNode);
Now, myNode
has newNode
in its children
, but newNode
does not have myNode
as its parent
. So you've just broken it.
(OFF-TOPIC) Why are you exposing the children anyway?
Yes, I could just create lots of methods: countChildren(), getChild(index), getChildrenIterator() (which returns a generator), findChildIndex(node), and so on... but is it really a better approach than just returning an array, which provides an interface all javascript programmers already know?
- You can access its
length
to see how many children it has;
- You can access the children by their index (i.e.
children[i]
);
- You can iterate over it using
for .. of
;
- And you can use some other nice methods provided by an Array.
Note: returning a copy of the array is out of question! It costs linear time, and any updates to the original array do not propagate to the copy!
The solution
get children() { return Object.freeze(Object.create(this._children)); }
// OR, if you deeply care about performance:
get children() {
return this._PUBLIC_children === undefined
? (this._PUBLIC_children = Object.freeze(Object.create(this._children)))
: this._PUBLIC_children;
}
Done!
Object.create
: we create an object that inherits from this._children
(i.e. has this._children
as its __proto__
). This alone solves almost the entire problem:
- It's simple and fast (constant time)
- You can use anything provided by the Array interface
- If you modify the returned object, it does not change the original!
Object.freeze
: however, the fact that you can modify the returned object BUT the changes do not affect the original array is extremely confusing for the user of the class! So, we just freeze it. If he tries to modify it, an exception is thrown (assuming strict mode) and he knows he can't (and why). It's sad no exception is thrown for myFrozenObject[x] = y
if you are not in strict mode, but myFrozenObject
is not modified anyway, so it's still not-so-weird.
Of course the programmer could bypass it by accessing __proto__
, e.g:
someNode.children.__proto__.push(new Node());
But I like to think that in this case they actually know what they are doing and have a good reason to do so.
IMPORTANT: notice that this doesn't work so well for objects: using hasOwnProperty in the for .. in will always return false.
UPDATE: using Proxy to solve the same problem for objects
Just for completion: if you have an object instead of an Array you can still solve this problem by using Proxy. Actually, this is a generic solution that should work with any kind of element, but I recommend against (if you can avoid it) due to performance issues:
get myObject() { return Object.freeze(new Proxy(this._myObject, {})); }
This still returns an object that can't be changed, but keeps all the read-only functionality of it. If you really need, you can drop the Object.freeze
and implement the required traps (set, deleteProperty, ...) in the Proxy, but that takes extra effort, and that's why the Object.freeze
comes in handy with proxies.
freeze
ing the object will guard against code that [incorrectly] attempts to alter it. – CorroborateList<T>
interface. Let's say we create a new mutable object, say,l = new ArrayList<X>
. Now, let's say we doCollections.unmodifiableList(l)
which returns a new list, also ofList<T>
. However, this new list is immutable while the original list was mutable. Both conform toList<T>
. – Corroboratef = function (x) { return { value: function () { return x } } }; o = f(42)
- without changing the function named byo.value
(which would be prevented byfreeze
), there is no way to change whato.value()
evaluates to; hence there is immutability at the semantic level. Before arguing that this doesn't show a type exposing an immutable interface, consider Mock'ing that is sometimes done in statically typed languages. – Corroborate'use strict';
in all code where you want to find bugs while still developing it rather than after it’s causing some subtle bug in production ;-). – Oneiric