Object.at(), Object.size()

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.

I think there's a much better, indirect solution. First, Object.iterKeys() which would be an iterable of all the keys (not creating them in memory as an array) and then Iterator.prototype.size() and Iterator.prototype.at(). Then you could do Object.iterKeys(x).at(y) or Object.iterKeys(x).size(). It's doing the same thing and those are things people probably want anyway and you could repurpose it for that.

Iterators in general don’t have or know a size, so neither of those methods could go on Iterator.prototype.

There are already methods like this on Iterator.prototype, like map, reduce, toArray, every, filter, find, so I don't see why not.

Each of those operates on one item at a time, and do not need to know the number of yielded objects in advance.