reindexio / youmightnotneedunderscore

https://www.reindex.io/blog/you-might-not-need-underscore/
MIT License
227 stars 18 forks source link

Reflect.enumerate() appears deprecated #19

Open newswim opened 7 years ago

newswim commented 7 years ago

I was able to use this with Babel, but MDN says it'll get deleted any time.

Good alternative for "Names of all enumerable properties as an array"?

mbrowne commented 7 years ago

It looks like the only way to do this with a plain object now is the following:

Update: Unfortunately Object.entries() doesn't include inherited properties, so the Symbol.iterator approach is my only valid suggestion here (see discussion below).

const keys = Object.entries(obj).map(entry => entry[0])

Alternatively, if your objects are being created by a custom class, you could create a custom iterator method:

class Entity {
    *[Symbol.iterator]() {
        for (let key in this) {
            yield key;
        }
    }
}

var e = new Entity();
e.foo = 'foo';

Array.from( e[Symbol.iterator]() )  // ["foo"]

...but in many cases it would be preferable to just use a Map or Set, which are already iterable.

newswim commented 7 years ago

Will Object.entries() also return all of the keys including those of the prototype? This behavior seems to be what differentiates allKeys from Object.keys, and the only way I can find of doing so is by using a for..in loop, which is also what Underscore does.

Trying your class example, it seems like using a for-in within the class does not provide a new instance of that class any access to its parents' keys.

class Entity {
    *[Symbol.iterator]() {
        for (let key in this) {
            yield key;
        }
    }
}

var e = new Entity();
e.foo = 'do';

console.log(Array.from( e[Symbol.iterator]() ))
/*
Array [
  "foo"
]
*/

var f = Object.create(new Entity)
f.fee = 'fo'

console.log(Array.from( f[Symbol.iterator]() ))
/*
Array [
  "fee"
  // no 'foo'
]
*/
newswim commented 7 years ago

The bottom right region of this handy chart will not sate our thirst for answers, but will answer this conundrum.

newswim commented 7 years ago

"Not without extra code"

mbrowne commented 7 years ago

Ah, you're right about Object.entries() - I thought that included inherited properties but it doesn't.

But the Symbol.iterator approach does work with inherited properties (if you didn't need that, you could just use Object.keys()). It doesn't work in your example because foo is a property only added to the e object; it's never added to Entity.prototype (I should probably have given a more complete example; I see that you used my foo property as a starting point). Here's a working example:

class Entity {
    *[Symbol.iterator]() {
        for (let key in this) {
            yield key;
        }
    }
}

Entity.prototype.foo = 'f';

var f = new Entity();
f.bar = 'b';

Array.from( f[Symbol.iterator]() )  //["bar", "foo"]
mbrowne commented 7 years ago

I should add that I don't have much confidence that the above approach would be very useful in practice. The reason I tried this approach was this quote from an article on metaprogramming in ES6:

Update: This [Reflect.enumerate] was removed in ES2016 (aka ES7). myObject[Symbol.iterator]() is the only way to enumerate an Object’s keys or values now.

A Symbol.iterator method doesn't automatically exist in regular objects, at least not in the latest versions of Chrome and Firefox, so that's why I tried adding one to a base class. But if you're defining a method anyway, it would be a lot simpler to do it like this:

class Entity {
    allKeys() {
        let keys = [];
        for (let key in this) {
            keys.push(key);
        }
        return keys;
    }
}

The main difference when using an iterator is that you could loop over all enumerable properties using for..of, but we already have for...in for objects so that's kind of pointless.

The good news is that most enumerable properties are instance properties rather than properties that need to be defined on the prototype, so Object.keys() should work fine in most cases. If you always declare your data properties in the constructor then you probably won't encounter a need to loop over prototype properties in addition to instance properties (i.e. own properties). For example:

class Animal {
    constructor(name) {
        this.name = name || null;
    }
}

class Cat extends Animal {
    constructor(name) {
        super(name);
        this.numLegs = 4;
    }
}

var garfield = new Cat('Garfield');
Object.keys(garfield);  //["name", "numLegs"]