26

I'm wrapping an object in a Proxy and then iterate through it. How can I control the keys it iterates through?

The proxy works if I don't override the keys:

var obj = {"hello": "world"}
var proxy = new Proxy(obj, {})
for (var key in proxy){
    console.log(key)
}
// logs "Hello"

However, nothing is logged if I change the keys in the ownKeys handler.

var obj = {"hello": "world"}
var proxy = new Proxy(obj, {
    ownKeys: function(){
        return ["a", "b"]
    }
})
for (var key in proxy){
    console.log(key)
}
// Logs nothing

If I return "hello" as part of the ownKeys only "hello" is logged.

Apparently there was an enumerate trap in ES6, but it has been removed from ES7.

Is it still possible to control the for...in loop with a Proxy? Why was enumerate removed from the spec?

Matt Zeunert
  • 16,075
  • 6
  • 52
  • 78

3 Answers3

14

Unfortunately, it isn't possible to do this anymore.

As Brian Terlson (the editor of the EcmaScript Specification) wrote:

issue with proxy enumerate trap and for-in, where iimplementations are prevented from pre-populating the list of keys in the object, because the iterator causes observable affects. Which means the iterate must be pulled for every iteration. Last meeting we thought it would be ok if the enumerate trap exhausts the iterator, we thought that would solve the problem. The issue was, now their is an observable difference between an object and proxy of that object, mainly due to delete.

(Source: https://github.com/rwaldron/tc39-notes/blob/master/es7/2016-01/2016-01-28.md#5xix-proxy-enumerate---revisit-decision-to-exhaust-iterator via https://ecmascript-daily.github.io/2016/02/10/why-remove-enumerate-and-reflect-enumerate)

So it was removed due to technical challenges that could not be solved in a satisfactory manner.

has proxy trap

The in operator as such can still be captured using the has proxy trap:

var p = new Proxy({}, {
  has: function(target, prop) {
    if (prop === 'a') { return true; }
    return false;
  }
});
'a' in p; // true
'b' in p; // false

Alternative

As for (let key in proxy) loops are more of a legacy feature these days, you could use one of the following with the ownKeys proxy trap:

  • Object.keys() (own enumerable properties only)
  • Object.getOwnPropertyNames() (own properties)
  • Reflect.ownKeys() (own properties and Symbols)

enter image description here (Source: https://twitter.com/nilssolanki/status/659839340592422912)

(but you probably already knew that, seeing that you are working with proxies in the first place)

nils
  • 25,734
  • 5
  • 70
  • 79
  • Thanks Nils, awesome answer. I happen to be in an environment where I can use Babel to modify the code before execution, so I can add my custom logic that way. – Matt Zeunert Jul 26 '16 at 15:51
  • *I can use Babel to modify the code before execution, so I can add my custom logic that way.* I'm not entirely sure what you mean by that. Can you elaborate? – nils Jul 26 '16 at 16:17
  • Basically I can transform the code before it runs, so I can either replace the `for...in` loop with something else, or I can update it to contain my logic. It's not something you can do in most production situations, but I'm already running Babel on the code I'm working with, so it's not a problem. – Matt Zeunert Jul 26 '16 at 22:05
  • 2
    for (key in proxy) can be implemented. You need to implement ownKeys proxy trap as Nils said, and maybe you need to implement getOwnPropertyDescriptor proxy trap too to set them enumerable! – user2106769 Oct 30 '17 at 12:47
  • The [TC39 meeting minutes note can now be found here](https://github.com/rwaldron/tc39-notes/blob/master/es7/2016-01/jan-28.md#5xix-proxy-enumerate---revisit-decision-to-exhaust-iterator). – cpcallen Apr 05 '18 at 00:59
9

user2106769 gave the solution as a comment, but for anyone else like me who didn't see their comment, you can override for..in iteration with ownKeys and getOwnPropertyDescriptor:

var obj = { "hello": "world" };
var proxy = new Proxy(obj, {
    ownKeys: function() {
        return ["a", "b"];
    },
    getOwnPropertyDescriptor: function(target, key) {
        return { enumerable: true, configurable: true, value: this[key] };
    }
});
for (var key in proxy) {
    console.log(key);
}

EDIT: As Pärt Johanson mentioned, for correctness, "value" should be returned by getOwnPropertyDescriptor.

yeerk
  • 2,103
  • 18
  • 16
6

User user2106769 suggestion and yeerk's answer of overriding the getOwnPropertyDescriptor to allow enumeration of Proxy attributes has a flaw that one should be aware of when using it, it doesn't set the value attribute when trapping getOwnPropertyDescriptor, thus some other code that depends on that behavior will not function correctly.

The code demonstrating the flaw and the solution to it is below:

var obj = { "hello": "world" };
var flawedProxy = new Proxy(obj, {
    ownKeys: function() {
        return ["a", "b"];
    },
    getOwnPropertyDescriptor: function(target, key) {
         return { enumerable: true, configurable: true };
    }
});

var goodProxy = new Proxy(obj, {
    get: function(target, key) {
      // modify something here if you want to
      return target[key];
    },
    ownKeys: function() {
        return ["a", "b"];
    },
    getOwnPropertyDescriptor: function(target, key) {
         return { value: this.get(target, key), enumerable: true, configurable: true };
    }
});

// value is accessible, getOwnPropertyDescriptor not trapped
console.log(Object.getOwnPropertyDescriptor(obj, 'hello').value);

// value is undefined, getOwnPropertyDescriptor not trapped correctly
console.log(Object.getOwnPropertyDescriptor(flawedProxy, 'hello').value);

// value is accessible, getOwnPropertyDescriptor trapped correctly
console.log(Object.getOwnPropertyDescriptor(goodProxy, 'hello').value);
Pärt Johanson
  • 1,570
  • 1
  • 13
  • 13
  • Isn't that a bug of how the trap works? It seems like .value should be set.. – Zach Smith Jan 13 '20 at 11:06
  • This still seems overly simplified compared to what would be needed in production. Woudn't enumerable and configurable need to be false in some circumstances, and just overriding the whole object to be true may cause issues? – Ryan Leach Mar 08 '21 at 04:28