Почему свойства ES6 Symbol могут быть перечислены Object.defineProperty?

В свойствах ES6 можно определить как свойства символа:

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

MDN определяет перечислимые свойства как "те, которые могут быть итерированы циклом for..in" (1). Свойства символа никогда не повторяются с помощью цикла for... in, поэтому их можно считать неперечислимыми (2).

Значит ли это, что вы можете это сделать:

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

и что запрос объекта для этого дескриптора действительно подтверждает, что это свойство перечислимо:

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

Почему? Какая польза от этого?

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

(2) for... in использует [[Enumerate]], который включает только строковые ключи. Вероятно, теперь определение в MDN должно быть изменено, если у нас есть свойства символа.

Ответы

Ответ 1

Да, есть причина для перечисления свойств Symbol: 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 на Babel REPL.

Просто для ясности:

MDN определяет перечислимые свойства как "те, которые могут быть итерированы циклом for..in" (1).

Это просто неправильно для ES6 (ES2015). Это было разумное, хотя и упрощенное, определение в ES5 и более ранних версиях, но оно уже не было упрощено даже из-за Symbol s. Я исправил статью.


Это CW, потому что это был результат комментариев по этому вопросу.

Ответ 2

Это связано с тем, что правила для перечисления включают предложение, требующее строковые ключи. Имейте в виду, что перечисление и запрос ключей - это разные операции с совершенно разными правилами.

Рассматривая раздел для for ... in/for ... of оценки головы (13.7.5.12), он утверждает, что итерация выполнена с помощью:

  1. Если iterationKind перечисляется, то

    с. Верните obj.[[Enumerate]]().

В описании [[Enumerate]] (9.1.11) очень четко указано, что он:

Возвращает объект Iterator (25.1.1.2), метод next выполняет итерацию по всем строковым ключам перечислимых свойств O.

Проверка перечислимых свойств происходит позже в теле, а пример псевдокода делает это еще более ясным:

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
          }
      }
  }
  ...
}

(комментарии мои)

Очевидно, что свойства с нестрочными ключами не будут перечислены. Используя этот пример:

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);
}

вы можете проверить это в Вавилоне и Тракере.

Однако вы увидите две интересные вещи:

  • getOwnPropertyNames содержит неперечислимые свойства. Это имеет смысл, поскольку следует совершенно другим правилам.
  • for...in включает свойства, отличные от string, при обоих транспилерах. Это, похоже, не соответствует спецификации, но соответствует поведению ES5.