This is an observation of the WeakMap API and my experiences using it. The core observation is: it was always possible for the engine to implement WeakMap in two different ways.
- As hash map with weakly-held keys. (what engines do)
- As hidden properties on objects.
This idea would allow the user to ask the engine for an instance of WeakMap backed by hidden properties on objects. The resulting WeakMap instance would be indistinguishable from a normal one at the API level.
This implies a new weak flag in each property descriptor, which would be necessary to prevent the weakmap's hidden property descriptors from becoming visible using reflection APIs. A WeakMap implemented this way would hold a Symbol which it would never expose through its publc APIs. This would be the key used in the weak property descriptor. Thus, only the holder of the weak map would be able to read and write this data. Since the symbol would be invisible to reflection, it would not be possible to gain access to it and forge it. The ability to read and write the hidden property could still be delegated using getter and setter functions.
Why allow this? Well, it's a performance optimization. The default implementation is safe to use freely because it doesn't alter any hidden classes. Altering the hidden classes can have disasterous implications for performance if it isn't done with intention and care. This is to say: I think it makes sense that the default implementation is what it is.
To make safe and proper use of a an implementation that alters the hidden class, you'd need to use it like this:
let integritySet = new WeakSet([], { properties: true });
export class ClassWithIntegrity {
constructor() {
this.value = 'OK'; // hidden class altered on this line
integritySet.add(this); // hidden class altered on this line
Object.freeze(this);
}
}
Now you can do exactly this with the current WeakMap, and it works -- as I said, the APIs are identical. Unfortunately, the perf characteristics are not identical. If your usage is modeled on the cost of hidden properties in still-monomorphic objects, you will be sorely disappointed by the cost of maintaining a hash map, especially when the number of objects get into the millions. Hidden properties on monomorphic objects are still effortless from a perf standpoint, even with millions of objects. Weak hash maps are very much not effortless, and begin to consume (very) large amounts of time in garbage collecting, resizing, and reallocating the mapping table.
So I believe then the incentives are created for this proposal: both implementations shine, for different perf-related reasons, so long as they are used the way they are designed to perform well.
In practice this lets us efficiently do "parse don't validate" style coding with classes. Previously there was no mechanism for creating verifiable integrity that scaled to millions of object instances and beyond.
let integritySet = new WeakSet([], { properties: true });
export class ClassWithIntegrity {
constructor() {
// ...
}
logOK() {
if (!integritySet.has(this)) throw new TypeError();
console.log(this.value); // logs OK
}
}
Here's another example of how you can use this to create data integrity, even when you're working with mutable data.
let fooData = new WeakMap({ properties: true });
let barData = new WeakMap({ properties: true });
class DataAccessors {
constructor() {
fooData.set(this, 'foo');
barData.set(this, 'bar');
}
get foo() { return fooData.get(this); }
set foo(value) {
if (!value.startsWith('foo')) throw new Error();
return fooData.set(this, value);
}
get bar() { return barData.get(this); }
set bar(value) { return barData.set(this, value); }
}
Once again you could do this with the kind of WeakMap we already have, but once again you'd be hitting the perf use case that suffers most: implementing what should be a fast monomorphic struct access operation with slow hash map code and its internal operations like rebucketing that sometimes stop the world.
A final thought: the integrity use case, to function correctly, requires that the secret symbol used as the property key remain a secret. Without an engine-level implementation of propertyDescriptor.weak, it would be difficult to keep the symbol a secret. Luckily if the engine does not natively support properties: true, it can and will simply fall back to the map-based implementation, which will again provide integrity to the extent that the engine offers it. This time though the fallback has very broad support, so basically you could have runtime-backed integrity across almost any runtime, with perf that improves rapidly as runtimes implement properties: true.