22

In ES6 properties can be defined as symbol properties:

var symbol = Symbol();
var object = {};
object[symbol] = 'value';

MDN defines enumerable properties as 'those which can be iterated by a for..in loop' (1). Symbol properties are never iterated by a for...in loop, therefore they can be considered non-enumerable (2).

Does it make any sense, then, that you can do this:

Object.defineProperty(object, symbol, {
    value: 'value',
    enumerable: true
});

and that querying object for it's descriptor does indeed confirm that this property is enumerable:

Object.getOwnPropertyDescriptor(object, symbol)
// -> { enumerable: true }

Why? What use is this?

(1) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties

(2) for...in uses [[Enumerate]], which only includes string keys. Probably the definition on MDN should be changed now that we have symbol properties.

stephband
  • 2,594
  • 2
  • 23
  • 33
  • Why shouldn't it be allowed? You could asked the reverse: Why can normal properties by made non-enumerable if they are enumerable by default? Note sure I understand where you are going with this... – Felix Kling Jun 24 '15 at 14:48
  • Does an enumerable property with a symbol for its key show up in the `for ... in` iteration? (Seems like that would be problematic.) – Pointy Jun 24 '15 at 14:49
  • 1
    The point is that they are still not enumerable - they still don't get enumerated in for...in or in Object.keys - so to all intents and purposes they are non-enumerable, but they say that ARE enumerable in the descriptor. – stephband Jun 24 '15 at 14:50
  • It seems like your question boils down to, why does `[[Enumerate]]` only list string properties, not all enumerable properties? – loganfsmyth Jun 24 '15 at 14:50
  • No, my question is why should defineProperty allow you to declare enumerable: true on a symbol? – stephband Jun 24 '15 at 14:51
  • I can iterate over enumerbale symbols in Firefox. You can't? *edit:* Ugh, doesn't seem to work in Chrome though. – Felix Kling Jun 24 '15 at 14:52
  • @Felix oh, can you? Hang on, I must test that again... – stephband Jun 24 '15 at 14:53
  • 1
    @FelixKling: You can? [I can't](http://jsfiddle.net/mwd3960r/), with FF38. The spec says [it shouldn't](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-ordinary-object-internal-methods-and-internal-slots-enumerate). – T.J. Crowder Jun 24 '15 at 14:55
  • @Felix no, you can't. not in FF 38, anyway. – stephband Jun 24 '15 at 14:55
  • It's an interesting question; it's almost as if the `enumerable` flag doesn't affect symbol-keyed properties. Whether the flag is set or not, the key is returned from `getOwnPropertySymbols()`. – Pointy Jun 24 '15 at 14:56
  • Enumerability is still potentially a useful thing to know for a symbol, whether or not it makes it work in a for...in loop. – loganfsmyth Jun 24 '15 at 14:56
  • @Pointy The `[[Enumerate]]` internal method is what drives `for...in` and it only returns string property keys. – loganfsmyth Jun 24 '15 at 14:57
  • @loganfsmyth But if it doesn't enumerate in a for...in loop (or anywhere else), then it's not enumerable. So I don't see how it is useful. – stephband Jun 24 '15 at 14:57
  • @loganfsmyth right, sure; the point is that that's true regardless of the `enumerable` flag value, but also `enumerable` doesn't hide the property key from `getOwnPropertySymbols()`. – Pointy Jun 24 '15 at 14:59
  • OK, my bad. I thought `Symbol.isGenerator` (what I used) is a special symbol, but it's some kind of reflection method on functions :-/ (in FF). – Felix Kling Jun 24 '15 at 14:59
  • @stephband: Well, I guess letting them be "enumerable" leaves the door open to having some form of enumeration operation in the future that includes them, even though they're excluded from the current ones. – T.J. Crowder Jun 24 '15 at 15:01
  • @Pointy Correct, the `getOwnerProperty*` functions are not affected by enumerability. – loganfsmyth Jun 24 '15 at 15:01
  • Yeah I'm not sure what more I can say. It does seem like MDN should be updated. – loganfsmyth Jun 24 '15 at 15:03
  • FWIW, if you do `foo[symbol] = bar;`, then the property descriptor also says the symbol is enumerable. Property descriptors just seem to be unaware of the "type" of property, which is fine IMO. Symbols are simply always non-enumerable, regardless of what the flag says. It was probably simpler to keep existing existing behavior as it is, if changing it doesn't add any additional value. – Felix Kling Jun 24 '15 at 15:04
  • @Felix: Oh, that's interesting actually. Related question – should Object.assign copy across only enumerable symbol properties to the target object, or all symbol properties? – stephband Jun 24 '15 at 15:07
  • The spec says *"The assign function is used to copy the values of all of the enumerable own properties from one or more source objects to a target object."* And the algorithm calls `OwnPropertyKeys`. So I guess the answer is it does not copy symbols. – Felix Kling Jun 24 '15 at 15:08
  • @Felix 'Both String and Symbol properties are copied.' - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign – stephband Jun 24 '15 at 15:10
  • @Felix but you're right, it does say only OWN and ENUMERABLE properties, so I guess if a symbol property is made non-enumerable it should not get copied. – stephband Jun 24 '15 at 15:11
  • 1
    So just to be clear [`[[OwnPropertyKeys]]`](http://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys) returns symbols as well. `Object.assign` simply looks at the enumerable flag, no matter whether the value is a normal key or a symbol. So, yeah, since a symbol's `enumerable` flag is also `true` by default, they are copied. It seems that they are only specially treated in `for/in/of` loops. – Felix Kling Jun 24 '15 at 15:17
  • Related question: [Why bring symbols to javascript?](http://stackoverflow.com/questions/21724326/why-bring-symbols-to-javascript) – Yogi Jun 24 '15 at 15:20

2 Answers2

21

Yes, there's a reason for allowing Symbol properties to be enumerable: Object.assign:

let s1 = Symbol();
let s2 = Symbol();
let s3 = Symbol();
let original = {};
original[s1] = "value1";                // Enumerable
Object.defineProperty(original, s2, {   // Enumerable
  enumerable: true,
  value: "value2"
});
Object.defineProperty(original, s3, {   // Non-enumerable
  value: "value3"
});
let copy = {};
Object.assign(copy, original);
console.log("copy[s1] is " + copy[s1]); // value1, because it was enumerable
console.log("copy[s2] is " + copy[s2]); // value2, because it was enumerable
console.log("copy[s3] is " + copy[s3]); // undefined, because it wasn't enumerable

Live Copy on Babel's REPL.

Just for clarity:

MDN defines enumerable properties as 'those which can be iterated by a for..in loop' (1).

That's simply wrong for ES6 (ES2015). It was a reasonable, if simplistic, definition in ES5 and earlier, no it's no longer even simplistically correct because of Symbols. I've fixed the article.


This is a CW because it was the outgrowth of the comments on the question.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 2
    Aha! Now that is useful, because my original question came up while writing a polyfill for Object.assign. Thanks. – stephband Jun 24 '15 at 15:18
  • So I guess this already adds more confusion to the question that "What is `enumerable`?" I believe the most simplistic answer is that what's `enumerable` is _visible_. In other words, if you `console.log` an object, whatever you see can be considered `enumerable` and vice versa. – Saeed Ahadian Dec 17 '19 at 18:12
  • 1
    @SaeedAhadian - I wouldn't put it that way, because A) If something's not "visible," I expect code trying to use it to fail, but it wouldn't. B) Most consoles I know show non-enumerable properties. Certainly the ones in Chrome/Chromium/Brave, Firefox, IE11, Edge (old JScript version), and Edge (beta V8 version) all do. Node.js's doesn't (which surprised me). – T.J. Crowder Dec 17 '19 at 18:21
  • @T.J.Crowder Working with Node.js, I was totally oblivious to this fact that even non-enumerable properties appear in other consoles like Chrome. So I always used that trick for finding what's enumerable and what's not which now seems not always a good measure. – Saeed Ahadian Dec 17 '19 at 18:43
  • @SaeedAhadian - :-) You can use [`Object.entries(obj)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries) instead (assuming you only care about *own* enumerable properties). Or `JSON.stringify` if the object doesn't have any cyclic references. – T.J. Crowder Dec 17 '19 at 19:07
6

This is because the rules for enumeration include a clause requiring string keys. Bear in mind that enumeration and asking for keys are different operations with entirely different rules.

Looking at the section for for ... in/for ... of head evaluation (13.7.5.12), it states that the iteration is done using:

  1. If iterationKind is enumerate, then

    c. Return obj.[[Enumerate]]().

The description of [[Enumerate]] (9.1.11) very clearly states that it:

Return an Iterator object (25.1.1.2) whose next method iterates over all the String-valued keys of enumerable properties of O.

The check for enumerable properties comes later in the body, and the pseudo-code example makes this even more clear:

function* enumerate(obj) {
  let visited=new Set;
  for (let key of Reflect.ownKeys(obj)) {
      if (typeof key === "string") { // type check happens first
          let desc = Reflect.getOwnPropertyDescriptor(obj,key);
          if (desc) {
              visited.add(key);
              if (desc.enumerable) yield key; // enumerable check later
          }
      }
  }
  ...
}

(comments mine)

Clearly, properties with non-string keys will not be enumerated. Using this example:

var symbol = Symbol();
var object = {};

Object.defineProperty(object, symbol, {
    value: 'value',
    enumerable: true
});

Object.defineProperty(object, 'foo', {
  value: 'bar',
  enumerable: true
});

Object.defineProperty(object, 'bar', {
  value: 'baz',
  enumerable: false
});

Object.defineProperty(object, () => {}, {
  value: 'bin',
  enumerable: true
});

for (let f in object) {
  console.log(f, '=', object[f]);
}

for (let k of Object.getOwnPropertyNames(object)) {
  console.log(k);
}

you can verify that in Babel and Traceur.

However, you'll see two interesting things:

  1. getOwnPropertyNames includes non-enumerable properties. This makes sense, as it follows completely different rules.
  2. for...in includes non-string properties under both transpilers. This does not seem to match the spec, but does match ES5's behavior.
Thomson
  • 20,586
  • 28
  • 90
  • 134
ssube
  • 47,010
  • 7
  • 103
  • 140
  • Thank you @ssube. This is useful technical info. I marked T.J. Crowder's answer as the answer because it addresses the 'Why?', but this answer does address the specced 'Why?', so I think both are equally useful. – stephband Jun 24 '15 at 15:24
  • @stephband That's completely fair. This is a very literal why. – ssube Jun 24 '15 at 15:44