原文链接:http://javascript.info/symbol,translate with ❤️ by zhangbao.

根据规范,对象属性可以是字符串,也可以是 Symbol 值。非数字,非布尔值,仅支持字符串和 Symbol 这两种类型。

到现在为止,我们只学习了字符串。现在我们来看看使用 Symbol 给我们带来的好处。

Symbol

“Symbol”表示一个独一无二的标识。

我们使用 Symbol() 来创建这一类型的值:

  1. // id 是一个 Symbol 类型值
  2. let id = Symbol();

我们也可以为生成的 Symbol 值添加一个描述(也被称为 Symbol 名称),来方便 debug。

  1. // id 是一个带有描述内容 "id" 的 Symbol 值
  2. let id = Symbol("id");

Symbol 能保证是唯一的。即使我们用同一个描述内容创建了许多个 Symbol 值,它们彼此也是不同的值。描述只是一个不影响任何东西的标签。

例如,下面两个 Symbol 值具有同样的描述,但它们就是不一样:

  1. let id1 = Symbol("id");
  2. let id2 = Symbol("id");
  3. alert(id1 === id2); // false

如果你熟悉 Ruby 或者其他有某种“Symbol”的语言,请不要被误导。JavaScript Symbol 是不同的。

Symbol 值不会自动转换成字符串

JavaScript 中的许多值支持隐式的转换成字符串。例如,我们可以 alert 几乎任何值,都能正常工作。Symbol 比较特别,它就不可以。

例如,alert 它的话会产生错误:

  1. let id = Symbol("id");
  2. alert(id); // TypeError: Cannot convert a Symbol value to a string

如果我们真的想要显示一个 Symbol,我们需要在它上面调用 toString() 方法,如下所示:

  1. let id = Symbol("id");
  2. alert(id.toString()); // Symbol(id), now it works

这是一种防止混乱的“语言卫士”,因为字符串和符号本质上是不同的,不应该偶尔将其转换成另一种。

“隐藏”属性

Symbol 可以帮助我们为对象创建隐藏属性,其他地方的代码不会偶然访问或者覆写它。

例如,如果我们想为 user 对象存储一个“标识符”,我们可以使用 Symbol 作为键名:

  1. let user = { name: "John" };
  2. let id = Symbol("id");
  3. user[id] = "ID Value";
  4. alert( user[id] ); // 我们可以是用 Symbol 类型值作为键名

那么使用 Symbol(‘id’) 比使用字符串 “id” 的好处是什么呢?

我们举个例子来更加深入的看下。

假设在另外一个脚本里的 user 对象用它自己的“id”属性,为了它的目的。这可能是另外一个 JavaScript 库,所以脚本之间完全不认识彼此。

那个脚本可以创建它自己的 Symbol(“id”),像这样:

  1. // ...
  2. let id = Symbol("id");
  3. user[id] = "Their id value";

注意,如果我们为了达到目的,使用的是字符串键名“id”而不是一个 Symbol 值,就会产生冲突了:

  1. let user = { name: "John" };
  2. // our script uses "id" property
  3. user.id = "ID Value";
  4. // ...if later another script the uses "id" for its purposes...
  5. user.id = "Their id value"
  6. // boom! overwritten! it did not mean to harm the colleague, but did it!

字面量中的 Symbol

如果我们想在对象字面量里使用 Symbol,就需要用到方括号了。

像这样:

  1. let id = Symbol("id");
  2. let user = {
  3. name: "John",
  4. [id]: 123 // not just "id: 123"
  5. };

因为我们是使用变量 id 作为键名的,而不是字符串“id”。

Symbol 类型会被 for…in 循环忽略的

Symbol 属性不会参与到 for..in 的循环里。

例如:

  1. let id = Symbol("id");
  2. let user = {
  3. name: "John",
  4. age: 30,
  5. [id]: 123
  6. };
  7. for (let key in user) alert(key); // name, age (没有 Symbol 值)
  8. // 通过 Symbol 值是可以直接访问的
  9. alert( "Direct: " + user[id] );

这是一般的“隐藏”概念的一部分。如果另外一个脚本或一个库在我们的对象上循环,它将不会意外地访问一个 Symbol 属性。

与此相反是,Object.assign 既会复制字符串类型和,也会复制 Symbol 类型的:

  1. let id = Symbol("id");
  2. let user = {
  3. [id]: 123
  4. };
  5. let clone = Object.assign({}, user);
  6. alert( clone[id] ); // 123

这并不是矛盾,只是设计使然。在克隆或者 Merge 一个对象的时候,我们通常希望复制所有的属性(包括 Symbol 类型值的 id)。

⚠️其他类型的属性键名会转换为字符串

在对象里,我们只能使用字符串或者 Symbol 值作为键名。其他类型的键名会自动转换为字符串。

例如,当数字 0 作为属性名使用的时候,会自动变成字符串 “0”。

  1. let obj = {
  2. 0: "test" // same as "0": "test"
  3. };
  4. // 两种方式访问的是同一个属性 (数字 0 转换成字符串 "0")
  5. alert( obj["0"] ); // test
  6. alert( obj[0] ); // test (same property)

全局 Symbol 值

我们知道,即使 Symbol 名称一样,但对应的 Symbol 值也是不同的。但有时我们希望相同名称的 Symbol 是相同的实体。

例如,我们应用里各个部分都需要一个叫“id”的 Symbol 值,并且期望得到的是同一个 Symbol 值。

为了实现这个,存在一个全局的 Symbol 注册表。我们在其中创建 Symbol 并且在稍后访问它,它能保证使用同一个名称访问到的 Symbol 是同一个值。

为了在注册表里创建和读取 Symbol,使用 Symbol.for(key)。

这个调用会检查全局注册表,如果有用 key 描述的 Symbol,就返回它;如果没有的话,就在全局注册表里使用 Symbol(key) 创建一个新的 Symbol 并返回。

例如:

  1. // 从全局注册表里读取
  2. let id = Symbol.for("id"); // 如果 Symbol 不存在,就创建
  3. // 再一次读取
  4. let idAgain = Symbol.for("id");
  5. // the same symbol
  6. alert( id === idAgain ); // true

注册表里的 Symbol 称之为全局 Symbol。如果我们想一个应用级别的 Symbol,在应用里的任何地方都可以访问的话,这就是符合使用它的场景。

⚠️听起来像 Ruby

在一些编程语言中,比如 Ruby,每个名称都仅对应一个 Symbol。

在 JavaScript 中,我们可以看到,这是全局符号的权利。

Symbol.keyFor

对应全局 Symbol,不仅可以用 Symbol.keyFor(key) 返回指定名称的 Symbol,也可以反过来调用:Symbol.keyFor(sym),这是做反向操作:通过给定的全局 Symbol 值返回对应的 key。

例如:

  1. let sym = Symbol.for("name");
  2. let sym2 = Symbol.for("id");
  3. // 通过 Symbol 得到名称
  4. alert( Symbol.keyFor(sym) ); // name
  5. alert( Symbol.keyFor(sym2) ); // id

Symbol.keyFor 内部会在全局 Symbol 注册表里查找指定 Symbol 对应的 key。因此对于非全局 Symbol 它是不起作用的,并且会返回 undefiend。

例如:

  1. alert( Symbol.keyFor(Symbol.for("name")) ); // name, 全局 symbol
  2. alert( Symbol.keyFor(Symbol("name2")) ); // undefined, 参数不是一个全局 symbol

系统 Symbol 值

JavaScript 内部里存在许多“系统”Symbol 值,我们可以用它们来调整我们对象的各个方面。

在规范的 Well-known symbols 这张表里有列举:

  • Symbol.hasInstance

  • Symbol.isConcatSpreadable

  • Symbol.iterator

  • Symbol.toPrimitive

  • 等等

例如,Symbol.toPrimitive 允许我们自定义对象的转换规则。我们很快就能看到了。

其他的 Symbol 值会在之后学习对应的特性中,进行介绍说明。

总结

Symbol 是作为唯一标识符使用的原始类型值。

Symbol 值使用 Symbol() 创建,可以附加可选的描述参数。

Symbol 值总是不同的,即使它们有着相同的名称。如果我们想要名称相等即相等的 Symbol 值,就要用到全局注册表了:Symbol.for(key) 根据指定名称,返回(没有就创建)一个全局 Symbol 值。多次调用 Symbol.for 的结果仅返回同一个 Symbol 值(在名称相同的情况下)。

Symbol 主要有两个使用场景:

  1. 作为对象的“隐藏”属性。如果我们为一个对象添加的属性名在另一个脚本里也“存在”的话,我们就可以用一个 Symbol 值来作为属性名创建属性。Symbol 属性不会出现在 for…in 循环遍历中,所以它不会偶尔被列出。它越不能直接访问,因为另一个脚本里没有我们的 Symbol 值,因此,它不会偶尔干预它的行动。

我们可以秘密地使用 Symbol 类型属性名在对象里隐藏一些属性,这样别人就看不到了。

  1. JavaScript 提供了许多系统 Symbol 值,通过 Symbol.* 的形式访问。我们可以使用它们来修改一些内置行为。例如,在之后非教程里我们会在可迭代对象中使用 Symbol.iterator,在对象转换到原始类型值的时候使用 Symbol.toPrimitive 等等。

技术上讲,Symbol 值并不能 100% 隐藏。这里提供了一个内置方法 Object.getOwnPropertySymbols(obj) 来获得一个对象身上的所有的 Symbol 属性。Reflect.ownKeys(obj) 方法返回一个对象身上的所有属性(包括 Symbol 属性值)。因此他们并不是真的隐藏了。但是大多数的库、内置的方法和语法结构都遵循一个共同的协议。明确地调用前面提到的方法的人可能很清楚他在做什么。

(完)