为什么ES6的Symbol属性可以通过Object.defineProperty方法被设置为可枚举的?

22

在ES6中,属性可以被定义为符号属性:

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

MDN将可枚举属性定义为“可以通过for..in循环迭代的属性”(1)。Symbol属性永远不会被for...in循环迭代,因此它们可以被视为不可枚举的属性(2)。

那么,你可以这样做,这有意义吗:

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

查询该对象的描述符确实确认了该属性是可枚举的:

Object.getOwnPropertyDescriptor(object, symbol)
// -> { enumerable: true }
为什么?这有什么用处?
(1) https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Enumerability_and_ownership_of_properties (2) for...in 循环使用的是 [[Enumerate]],只会包括字符串键。现在我们已经有了 symbol 属性,因此 MDN 上的定义可能需要更改。

为什么不允许呢?你可以反过来问:如果默认情况下它们是可枚举的,为什么普通属性不能被设置为不可枚举的呢?我不确定你的意思是什么... - Felix Kling
1
重点是它们仍然不可枚举 - 它们仍然不会在 for...in 或 Object.keys 中被枚举 - 因此从所有意义上讲,它们是不可枚举的,但它们在描述符中被标记为可枚举。 - stephband
看起来你的问题归结为,为什么[[Enumerate]]只列出字符串属性,而不是所有可枚举属性? - loganfsmyth
1
@FelixKling:你可以吗?我不行,在FF38中。规范说不应该 - T.J. Crowder
1
所以,只是为了明确一点,[[OwnPropertyKeys]] 也会返回符号。Object.assign 只是查看可枚举标志,无论该值是普通键还是符号。所以,是的,由于符号的 enumerable 标志默认也为 true,它们被复制了。似乎它们只在 for/in/of 循环中受到特殊处理。 - Felix Kling
显示剩余20条评论
2个回答

21

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

请在Babel的REPL上查看实时复制
为了明确起见:
MDN将可枚举属性定义为“可以通过for..in循环迭代的属性”(1)。这在ES5及之前是合理的,但对于ES6(ES2015)来说,这只是一个过于简单化的定义,因为有了Symbol。我已经修正了文章。
这是一个CW,因为它是问题评论的产物。

2
啊哈!现在这很有用,因为我最初的问题是在编写Object.assign的polyfill时遇到的。谢谢。 - stephband
所以我猜这会使“什么是enumerable?”这个问题更加混乱。 我认为最简单的答案是enumerable是可视的。换句话说,如果您用console.log打印一个对象,您看到的任何内容都可以被视为enumerable,反之亦然。 - Saeed Ahadian
1
@SaeedAhadian - 我不会这样说,因为A)如果某些东西不“可见”,我期望试图使用它的代码失败,但实际上并没有。B)我知道的大多数控制台都显示非枚举属性。当然,Chrome / Chromium / Brave,Firefox,IE11,Edge(旧版JScript版本)和Edge(beta V8版本)中的控制台都是如此。Node.js的不是(这让我感到惊讶)。 - T.J. Crowder
@T.J.Crowder 在使用Node.js时,我完全没有意识到即使是非枚举属性也会出现在其他控制台(如Chrome)中。因此,我总是使用那个技巧来查找什么是可枚举的,什么不是,但现在看来这并不总是一个好的衡量标准。 - Saeed Ahadian
@SaeedAhadian - :-) 你可以使用Object.entries(obj)代替(假设你只关心自有可枚举属性)。如果对象没有任何循环引用,也可以使用JSON.stringify - T.J. Crowder

6
这是因为枚举规则包括要求字符串键的条款。请记住,枚举和请求键是完全不同的操作,具有完全不同的规则。
查看“for ... in”/“for ... of”头部评估(13.7.5.12)的部分,它指出迭代使用以下方式完成:
  1. 如果iterationKind是enumerate,则

    c. 返回obj.[[Enumerate]]()

[[Enumerate]]的描述(9.1.11)非常清楚地说明了它:
返回一个迭代器对象(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);
}

你可以在Babel和Traceur中验证这一点。
但是,你会看到两件有趣的事情:
  1. getOwnPropertyNames 包括不可枚举属性。这很有道理,因为 它遵循完全不同的规则
  2. for...in 在两个转换器下都包括非字符串属性。这似乎不符合规范,但与ES5的行为相匹配。

谢谢@ssube。这是有用的技术信息。我将T.J. Crowder的答案标记为答案,因为它回答了“为什么?”,但是这个答案确实回答了规定的“为什么?”,所以我认为两者都同样有用。 - stephband
@stephband 这完全是公平的。这是一个非常字面的原因。 - ssube

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接