tc39/proposal-object-rest-spread

Use object iterator, if present

getify opened this issue · 19 comments

If you use ...o in a spread context and the o in question has defined a Symbol.iterator, it would make more sense to use that than to just iterate own properties, in the same way that arrays work.

So, it should first check for the Symbol.iterator, and use it if present, otherwise fall back to the own-keys iteration.

What do you mean, use it if present. What would the interator return,
string keys?

On Wednesday, 8 April 2015, Kyle Simpson notifications@github.com wrote:

If you use ...o in a spread context and the o in question has defined a
Symbol.iterator, it would make more sense to use that than to just
iterate own properties, in the same way that arrays work.

So, it should first check for the Symbol.iterator, and use it if present,
otherwise fall back to the own-keys iteration.

Reply to this email directly or view it on GitHub
#2.

Sebastian McKenzie

What do you mean, use it if present.

Something like:

var o = {
   [Symbol.iterator]: function*() {
      yield "a";
      yield "c";
   },
   a: 1,
   b: 2,
   c: 3
};

var keysToCopy = (function(){
   if (Symbol.iterator in o) return [...o];
   else return Object.getOwnPropertyNames(o);
})();

keysToCopy; // ["a","c"]

What would the interator return, string keys?

Initially, that was my thought, yes, as above.

But it would be even more powerful, and make more sense in line with arrays and other iterables, if it could actually act like the entries() iterator on collections:

var o = {
   [Symbol.iterator]: function*() {
      yield ["a", 10];
      yield ["c", this.c];
   },
   a: 1,
   b: 2,
   c: 3
};

var propsToCopy = (function(){
   if (Symbol.iterator in o) return [...o];
   else if (o.entries) return o.entries();
   else {
      return Object.getOwnPropertyNames(o).map(function(k){
         return [k,o[k]];
      });
   }
})();

propsToCopy; // [ ["a",10], ["c",3] ]

In other words, with arrays and other iterables, I currently have the freedom to completely customize what the result of the ... spread is, so it'd make most sense if I could do the same with any object or collection.

The default behavior of ...o would still be to get the enumerable own properties (or whatever), but it should be possible to override that if I want to.

I'd personally rather have the clear separation where objects are rest and spread using their simple properties, as they are a key-value mapping, but sequences such as arrays or function args using their iterator.

If we do want greater control over how an object is to be rest and spread, it seems like overloading Symbol.iterator is not the appropriate solution. Perhaps another Symbol such as Symbol.objectRest.

I think trying to abuse Symbol.iterator is a conflation of concerns for the problem at hand.

So if I had some custom class that had a Symbol.iterator that returned something unique to that class, how would this work?

class BensCoolClass {
 *[Symbol.iterator]() {
   yield 'wooooo!'
 } 

 someProp = "hi there"
}

var awesome = new BensCoolClass();

var whatNow = { lol: 'wut', ...awesome };

Your question really has nothing to do with class... it's simply a question of the context in which ... is used on an object value.

If we went with my suggestion, of forcing an object's iterator to have to be like the entries(..) iterator, when doing spread ...awesome inside an object context, just as it is on other data structures, where the iterator has to return the key-value pairs, then the iterator you've presented should throw an error since it doesn't conform.

However, if you did ...awesome inside an array, or in the arguments list of a function call, or whatever, and you only returned 'woooo!', that's perfectly fine, since in those contexts keys aren't needed.

Your question really has nothing to do with class

Obviously. It was just an example. I could have a factory that produced some fancy object with a custom iterator impl, or anything of the like. It seems like your suggestion would break in this case.

I have a counter suggestion which is to call a Symbol.enumerator method and pull down an iterator that behaves as you suggest. I'm flexible on the name, and that could be bike-shedded all you want, but that way people aren't forced to choose between having for..of work they way they want for their type and supporting Object rest spreads.

... a use case for what I'm saying above is perhaps something like a data row from a database maybe? You might want for..of to iterate over the values in the row, but have the Object spread set them in the target object by key and value.

I would say if you had different iteration/enumeration needs, your object could provide different methods just like the array, Map, etc do, like entries(..), values(..), etc. Then we'd just need to bikeshed on what the default iterator should be, exactly like they had to decide on defaults for array, Map, etc.

You could then do:

var x = { a: 1, b: 2 };

[ ...x ]; // [ 1, 2 ]
[ ...x.values() ]; // [ 1, 2 ]
[ ...x.keys() ]; // [ "a", "b" ]
[ ...x.entries() ]; // [ [ "a", 1 ], [ "b", 2 ] ]

( { ...x, c: 3 } ); // { a: 1, b: 2, c: 3}
( { ...x.entries(), c: 3 } ); // { a: 1, b: 2, c: 3 }

That's not bad.

But to be clear, you're proposing that Object.prototype have a default Symbol.iterator to make this "just work", right? From there, a user could implement their own Symbole.iterator method on a custom object/class if they wanted override it to some custom behavior (i.e. making some properties not copy over, Angular 1.X's properties beginning with $ come to mind). Do I understand your proposal correctly?

One thought, we might have to define a type that the iterator returns... like a PropertyValue<K, V> { key: K, value?: V } or something?

Do I understand your proposal correctly?

Sorta. If it could be default that objects could have entries(..) and values(..) and keys(..), that'd be awesome. That might be a tough sell to add that to the Object.prototype, for possible overlap with people having done those methods themselves.

So, more conservatively, I'm really suggesting that you should be able to define your own entries(..), values(..), and keys(..) as you see fit (no defaults per se), and that ... would select the appropriate default when used in the various different { ...x } and [ ...x ] contexts.

Another option would be to define an object-iterable prototype to opt-in to linkage to (could also be done more nicely with class, ofc), like:

var x = Object.assign(Object.create(Object.iterablePrototype), {
   a: 1,
   b: 2
}));

[ ...x.entries() ]; // [ [ "a", 1 ], [ "b", 2 ] ]

That way you can opt an object into default sensible iterations, and then decide to override those as you see fit.

Also, keep in mind that entries(..), keys(..), and values(..) are in fact functions, so you could even define your interfaces so that you can pass arguments to them to further control their customizations.

That might be a tough sell to add that to the Object.prototype, for possible overlap with people having done those methods themselves.

This is precisely the use case for Symbols. With these immutable keys, overlap would be extremely unlikely, in practice it should never happen. It's one of the reasons why Symbol.iterator is great for interop.

I like this new idea. You could use Symbols
Symbol.keys Symbol.values and Symbol.entries.

But that may be overkill. Perhaps just Symbol.entries would suffice, and it could return an iterator of entries?

Total aside, do you know if ... works with iterators? That could be interesting.

Semantically, maybe Symbol.properties is better? Total bike shed material

(Cross-posted from #1 with a few edits/additions)

The needed behavior here could be done via a proxy wrapper (which accepts any iterable):

function withEnumerator(object, f = Reflect.ownKeys) {
  return new Proxy(object, {
    ownKeys(target) {
      return [...f(target)]
    },

    getOwnPropertyDescriptor(target, key) {
      for (let i of f(target)) if (i === key) {
        return Object.getOwnPropertyDescriptor(target, i)
      }
    }
  })
}

let customObject = withEnumerator(new Class(), function *(target) {
  for (let i of Reflect.ownKeys(target)) {
    if (i !== 'someReallyHorribleProperty') {
      yield i
    }
  }
})

I think the best way would be something like Symbol.enumerate (like Symbol.iterator). It's purely additional, and it's something that could easily be transpiled/polyfilled. It's also almost completely independent of this proposal.

!function () {
  var hasOwn = Object.prototype.hasOwnProperty
  if (!hasOwn.call(Symbol, 'enumerate')) {
    Object.defineProperty(Symbol, 'enumerate', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: Symbol('Symbol.enumerate'),
    })
  }

  if (!hasOwn.call(Object.prototype, Symbol.enumerate)) {
    Object.defineProperty(Object.prototype, Symbol.enumerate, {
      configurable: false,
      enumerable: false,
      writable: false,
      value: function *enumerate() {
        for (let key of Reflect.ownKeys(target)) {
          yield {
            key,
            descriptor: Object.getOwnPropertyDescriptor(target, key),
          }
        }
      },
    })
  }

  // This is almost an exact clone of the O.[[Get]](P, receiver) algorithm, but
  // returns the descriptor, instead of the value
  // http://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-get-p-receiver
  function GetDescriptor(O, P, receiver) {
    const desc = Reflect.getOwnPropertyDescriptor(O, P)
    if (desc === undefined) {
      const parent = Object.getPrototypeOf(O)
      if (parent == null) return undefined
      return Reflect.get(parent, P, receiver)
    }
    if (hasOwn.call(desc, 'value') || hasOwn.call(desc, 'writable')) return desc
    const getter = desc.get
    if (getter === undefined) return undefined
    return getter.call(Receiver)
  }

  // Use this for spread properties
  function *enumerate(target) {
    for (let item of target[Symbol.enumerate]()) {
      const desc = %GetDescriptor(item, 'descriptor')
      yield {
        key: item.key,
        descriptor: desc !== undefined ? desc.value : {
          configurable: true,
          enumerable: true,
          writable: true,
          value: item.value,
        }
      }
    }
  }
}();

Example:

const prop = (o, key) => {key, value: o[key]}

class Class {
  constructor() {
    this.prop = 1
  }

  get inheritedProperty() { return 2 }

  get badProperty() { return 3 }

  *[Symbol.enumerate]() {
    yield* super[Symbol.enumerate](target)

    yield {key: 'inheritedProperty', value: this.inheritedProperty}
    yield {key: 'nonenumerable', descriptor: {
      configurable: true,
      enumerable: false,
      writable: true,
      value: 'Hello!',
    }}
  }
}

var inst = new Class()
inst.prop === 1
inst.inheritedProperty === 2
inst.badProperty === 3
inst.nonenumerable === undefined

// Logs:
// prop
// inheritedProperty
for (let i of inst[Symbol.enumerate]()) {
  console.log(i)
}

var obj = {...inst}
obj.prop === 1
obj.inheritedProperty === 2
obj.badProperty === undefined
obj.nonenumerable === 'Hello!'

(obj instanceof Class) === false

What do you all think?

I think it's weird to have iterators on arrays (called by the ... operator), but enumerators on objects when used with the same operator. IMO, the precedence of values(), keys(), and entries() still feels like the right path to mirror from array rest/spread to object rest/spread.

Please, could someone explain, why is there no (optional) 'key' property in iterator response? Like that:

{
  done: true,
  key: 'bar',
  value: 10
}
pitaj commented

[Symbol.iterator] doesn't make sense when the target is an object. Instead, the properties should be copied. It isn't that hard to do what you desire. Just add ...[ ].

let o = {
  [Symbol.iterator]: function* () {
    yield ["a", 10];
    yield ["c", this.c];
  },
  a: 1,
  b: 2,
  c: 3,
};

let p = { ...[...o] };

Perhaps if there were a Symbol.propertyIterator or something, this would make more sense to me.

I think there are use-case collisions with using Symbol.iterator.

I think adding something like that might dilute the proposal though, as it would add a lot of scope to it.

It seems bad to overload Symbol.iterator on custom objects to mean either keys or entries since most of the time you want to implement values semantics to work well with for...of. Symbol.propertyIterator seems like the better option to me.

Going to close this out on this proposal and leave this as a possible future enhancement since Symbol.propertyIterator or custom for...in enumeration can be a standalone proposal and doesn't need to block this.

If you want this you can just use a proxy anyway:

var proxy = new Proxy({  foo: "yes", bar: "wow", foobar: "asdf" }, {
  ownKeys() {
    return ["foo", "bar"];
  }
});

var { foo, ...rest } = proxy;
foo === "yes";
rest.bar === "wow";
rest.foobar === undefined;