2

I need to exercise some code in data mapping functions. For this, I need a proxy that returns an iterable array with one element (itself; an iterable array) when any property is requested. This will activate all loops and property checks in the data mapper layer (not shown here).

Something like this:

p = new Proxy([], {
  get(obj, prop) {
    if(prop === 'length') return obj.length
    if(prop === 'push') return obj.push
    if(p.length === 0) p.push(p)
    return p
  }}
)

This starts off well:

> p.any.length
1

but fails here:

> for(let v of p.any){console.log(v)}
Uncaught TypeError: p.any is not iterable

I need the loop to iterate and console.log one element:

> p.any
Proxy [
  <ref *1> [ Proxy [ [Circular *1], [Object] ] ],
  { get: [Function: get] }
]

Some proxy docs and guides cover the in and object but do not cover the of and array.

jcalfee314
  • 4,642
  • 8
  • 43
  • 75
  • I think you need to look at the docs of the iteration protocol, not just proxy. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols – Barmar Jul 28 '23 at 22:55
  • 1
    Does this answer your question? [V8: ES6 proxies don't support iteration protocol when targeting custom objects?](https://stackoverflow.com/questions/41046085/v8-es6-proxies-dont-support-iteration-protocol-when-targeting-custom-objects) – pilchard Jul 28 '23 at 23:00
  • 1
    also see [Array.prototype.forEach() not working when called on a proxy with a get handler](https://stackoverflow.com/questions/40408917/array-prototype-foreach-not-working-when-called-on-a-proxy-with-a-get-handler) which discusses the iterator protocol – pilchard Jul 28 '23 at 23:01
  • But it might be useful to explain what you're actually trying to achieve here, because that first sentence in your post is hiding _a lot_ of details that would be good to make explicit, in case it turns out you _don't_ need a Proxy at all. – Mike 'Pomax' Kamermans Jul 28 '23 at 23:04
  • The proxy above may be tricky because I'm wrapping the array `[]` and not an object `{}`. I'm not sure what to do given those links. For the use-case: during unit testing, instead of sending live data in I send a proxy in to get an output object (convert to). It is just a bunch of functions that take an object and return an object (tree). – jcalfee314 Jul 28 '23 at 23:17
  • You say you want to return an array with one element. Then why do you return `p` instead of `[p]`? This circular reference where you do `p.push(p)` (so that `p[0] === p`) likely will mess up any data structure trying to work with that object. – Bergi Jul 29 '23 at 02:26
  • Because `p` is an array. The mapping layer may pull properties off of that array like `p.any` or may access the one element with `el of p`, where that one element is just what is needed to step into the next object in the mapping layer. – jcalfee314 Jul 30 '23 at 01:48

2 Answers2

3

The error occurs because the for..of loop will query the proxy object with the @@iterator symbol property, and your code will react by returning p, and not an iterator as is what Array.prototype[Symbol.iterator] returns.

The solution is simple: just like you let the access to length and push let through, also do the same with symbol properties.

Another thing you may consider is to not do p.push(p), but obj.push(p). That way you can ignore a push that is issued by the user, and can perform this action as the first action.

Resulting code:

const p = new Proxy([], {
  get(obj, prop) {
    if (obj.length === 0) obj.push(p);
    if (prop === 'length' || typeof prop === "symbol") return obj[prop];
    return p;
  }}
);

for (let v of p) {
  console.log(v);
}

console.log(p);
console.log(p.any);
console.log(p.any.any);

Maybe you should also let other property accesses pass through, like array methods (forEach, reduce, map, filter,... etc).

trincot
  • 317,000
  • 35
  • 244
  • 286
  • I wouldn't even check the length and conditionally fill the target on every single property access. Rather, when constructing the proxy, do `const target = []; const p = new Proxy(target, …); target[0] = p;` – Bergi Jul 29 '23 at 02:30
1

Basically with your code you overwrite ALL properties except push and length thus overwriting the iterator also!

To fix that you need to determine whether the requested prop belongs to the array or not.

If it's an existing property of the array or its prototypes use Reflect.get() to get the property's value, if not, return your proxy and add the proxy to the array if needed.

This means all existing properties of all the prototypes would work also!
For example we can check Object.prototype.propertyIsEnumerable.

const p = new Proxy([],{
    get(obj, prop, receiver) {

        // look the prop in the object and its prototypes
        if (prop in obj) {
          return Reflect.get(...arguments);
        }
        
        // the prop not found, return the proxy itself
        obj.length || obj.push(receiver);
        return receiver;
    },
});

// check whether we can call Object.prototype.propertyIsEnumerable 
console.log('Object.prototype.propertyIsEnumerable:', p.propertyIsEnumerable('any'));

console.log('p.any.length:', p.any.length);
console.log('p.any.any:', p.any.any);

console.log('iterating:');

for (let v of p.any) {
    console.log(v);
}
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17