Search code examples
javascriptforeachecmascript-6iterationes6-proxy

Array.prototype.forEach() not working when called on a proxy with a get handler


I have the following proxy:

const p = new Proxy({
  [Symbol.iterator]: Array.prototype.values,
  forEach: Array.prototype.forEach,
}, {
  get(target, property) {
    if (property === '0') return 'one';
    if (property === '1') return 'two';
    if (property === 'length') return 2;
    return Reflect.get(target, property);
  },
});

It's an array-like object, because it has numeric properties and the length property specifying the amount of elements. I can iterate it using a for...of loop:

for (const element of p) {
  console.log(element); // logs 'one' and 'two'
}

However, the forEach() method is not working.

p.forEach(element => console.log(element));

This code doesn't log anything. The callback function is never called. Why isn't it working and how can I fix it?

Code snippet:

const p = new Proxy({
  [Symbol.iterator]: Array.prototype.values,
  forEach: Array.prototype.forEach,
}, {
  get(target, property) {
    if (property === '0') return 'one';
    if (property === '1') return 'two';
    if (property === 'length') return 2;
    return Reflect.get(target, property);
  },
});

console.log('for...of loop:');
for (const element of p) {
  console.log(element);
}

console.log('forEach():');
p.forEach(element => console.log(element));
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.16.0/polyfill.min.js"></script>


Solution

  • One of the differences between a for...of loop and Array.prototype.forEach() is that the former uses the @@iterator property to loop over the object, while the latter iterates the properties from 0 to length, and executes the callback only if the object has that property. It uses a [[HasProperty]] internal method, which in this case returns false for every array element.

    The solution is to add also the has() handler, which will intercept the [[HasProperty]] calls.

    Working code:

    const p = new Proxy({
      [Symbol.iterator]: Array.prototype.values,
      forEach: Array.prototype.forEach,
    }, {
      get(target, property) {
        if (property === '0') return 'one';
        if (property === '1') return 'two';
        if (property === 'length') return 2;
        return Reflect.get(target, property);
      },
      has(target, property) {
        if (['0', '1', 'length'].includes(property)) return true;
        return Reflect.has(target, property);
      },
    });
    
    p.forEach(element => console.log(element));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.16.0/polyfill.min.js"></script>