The long explanation why Custom Elements can't return Proxy
is here:
The gist is Proxy
is too limited and the native HTMLElement has internal aspects that aren't exposed via just get
/set
.
I'm not as pessimistic about drain performance of objects with the introduction of Symbol.Indexer
. Basically, all processing of objects can stay the same but it's only that Objects that invoke a setter on obj[Symbol.Indexer]
would need special processing. For example, all named functioned should be "constructors" but V8 doesn't actually process any of that code until it's required, making them as fast as anonymous/lambda functions. At worst, you've degraded the performance of your object because you used custom indexes, but it shouldn't affect all objects. For some special cases, this may be worth it because there's no alternative*.
I took a look at V8's Fast Properties but while it explains the fast paths for object and arrays, it doesn't fully explain how an overloaded Array
object with custom properties would work:
const customIndexable = [1,2,3,4];
customIndexable.customMethod = () => console.log('foo');
At best I understood indexed values are stored as an array separately from the dictionary needed for keys anyway:
Handling of integer indexed properties is no less complex than named properties. Even though all indexed properties are always kept separately in the elements store, [...]
Meaning, the logistics for making it to work sound like it doesn't needs too much refactoring. V8 team already keeps them separate.
*But of course, I'm talking about my use cases where performance isn't as much as priority as replicating functionality.
For the original point about keys, I would think to do this with a Map (not including overloaded constructor
code):
class TrackedMap extends Map {
#keys = [];
set(key, value) {
if (!this.has(key)) {
this.#keys.push(key);
}
return Map.prototype.set.call(this, key, value);
}
delete(key, value) {
const indexOfKey = this.#keys.indexOf(key);
if (indexOfKey === -1) return false;
this.#keys.splice(indexOfKey, 1);
return Map.prototype.delete.call(this, key, value);
}
at(index) {
if (index > this.#keys.length) return undefined;
if (index < this.#keys.length * -1) return undefined;
return this.get(this.#keys.at(index));
}
}
FYI, Chrome
and Firefox
has a fast path for Array.indexOf
because it uses a HashMap after a certain point (Safari does not). So, it makes that look up O(1).
And this is just kinda wasteful since Map.prototype.keys
doesn't allow by index.
Tying it back, if index number as properties isn't just limited to Arrays, then it paves the way for all collections, including ReturnType<Map.prototype.keys>
to get index features, instead of polyfill/wrapping as explained above. The check could be Array.isArray(obj) || Symbol.indexer in obj
.
And just like myMap.keys()[Symbol.iterator]
is [native code]
, it can also be a userland function.