原文链接:http://javascript.info/symbol,translate with ❤️ by zhangbao.
根据规范,对象属性可以是字符串,也可以是 Symbol 值。非数字,非布尔值,仅支持字符串和 Symbol 这两种类型。
到现在为止,我们只学习了字符串。现在我们来看看使用 Symbol 给我们带来的好处。
Symbol
“Symbol”表示一个独一无二的标识。
我们使用 Symbol() 来创建这一类型的值:
// id 是一个 Symbol 类型值
let id = Symbol();
我们也可以为生成的 Symbol 值添加一个描述(也被称为 Symbol 名称),来方便 debug。
// id 是一个带有描述内容 "id" 的 Symbol 值
let id = Symbol("id");
Symbol 能保证是唯一的。即使我们用同一个描述内容创建了许多个 Symbol 值,它们彼此也是不同的值。描述只是一个不影响任何东西的标签。
例如,下面两个 Symbol 值具有同样的描述,但它们就是不一样:
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 === id2); // false
如果你熟悉 Ruby 或者其他有某种“Symbol”的语言,请不要被误导。JavaScript Symbol 是不同的。
Symbol 值不会自动转换成字符串
JavaScript 中的许多值支持隐式的转换成字符串。例如,我们可以 alert 几乎任何值,都能正常工作。Symbol 比较特别,它就不可以。
例如,alert 它的话会产生错误:
let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string
如果我们真的想要显示一个 Symbol,我们需要在它上面调用 toString() 方法,如下所示:
let id = Symbol("id");
alert(id.toString()); // Symbol(id), now it works
这是一种防止混乱的“语言卫士”,因为字符串和符号本质上是不同的,不应该偶尔将其转换成另一种。
“隐藏”属性
Symbol 可以帮助我们为对象创建隐藏属性,其他地方的代码不会偶然访问或者覆写它。
例如,如果我们想为 user 对象存储一个“标识符”,我们可以使用 Symbol 作为键名:
let user = { name: "John" };
let id = Symbol("id");
user[id] = "ID Value";
alert( user[id] ); // 我们可以是用 Symbol 类型值作为键名
那么使用 Symbol(‘id’) 比使用字符串 “id” 的好处是什么呢?
我们举个例子来更加深入的看下。
假设在另外一个脚本里的 user 对象用它自己的“id”属性,为了它的目的。这可能是另外一个 JavaScript 库,所以脚本之间完全不认识彼此。
那个脚本可以创建它自己的 Symbol(“id”),像这样:
// ...
let id = Symbol("id");
user[id] = "Their id value";
注意,如果我们为了达到目的,使用的是字符串键名“id”而不是一个 Symbol 值,就会产生冲突了:
let user = { name: "John" };
// our script uses "id" property
user.id = "ID Value";
// ...if later another script the uses "id" for its purposes...
user.id = "Their id value"
// boom! overwritten! it did not mean to harm the colleague, but did it!
字面量中的 Symbol
如果我们想在对象字面量里使用 Symbol,就需要用到方括号了。
像这样:
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // not just "id: 123"
};
因为我们是使用变量 id 作为键名的,而不是字符串“id”。
Symbol 类型会被 for…in 循环忽略的
Symbol 属性不会参与到 for..in 的循环里。
例如:
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age (没有 Symbol 值)
// 通过 Symbol 值是可以直接访问的
alert( "Direct: " + user[id] );
这是一般的“隐藏”概念的一部分。如果另外一个脚本或一个库在我们的对象上循环,它将不会意外地访问一个 Symbol 属性。
与此相反是,Object.assign 既会复制字符串类型和,也会复制 Symbol 类型的:
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
这并不是矛盾,只是设计使然。在克隆或者 Merge 一个对象的时候,我们通常希望复制所有的属性(包括 Symbol 类型值的 id)。
⚠️其他类型的属性键名会转换为字符串
在对象里,我们只能使用字符串或者 Symbol 值作为键名。其他类型的键名会自动转换为字符串。
例如,当数字 0 作为属性名使用的时候,会自动变成字符串 “0”。
let obj = {
0: "test" // same as "0": "test"
};
// 两种方式访问的是同一个属性 (数字 0 转换成字符串 "0")
alert( obj["0"] ); // test
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 并返回。
例如:
// 从全局注册表里读取
let id = Symbol.for("id"); // 如果 Symbol 不存在,就创建
// 再一次读取
let idAgain = Symbol.for("id");
// the same symbol
alert( id === idAgain ); // true
注册表里的 Symbol 称之为全局 Symbol。如果我们想一个应用级别的 Symbol,在应用里的任何地方都可以访问的话,这就是符合使用它的场景。
⚠️听起来像 Ruby
在一些编程语言中,比如 Ruby,每个名称都仅对应一个 Symbol。
在 JavaScript 中,我们可以看到,这是全局符号的权利。
Symbol.keyFor
对应全局 Symbol,不仅可以用 Symbol.keyFor(key) 返回指定名称的 Symbol,也可以反过来调用:Symbol.keyFor(sym),这是做反向操作:通过给定的全局 Symbol 值返回对应的 key。
例如:
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 Symbol 得到名称
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor 内部会在全局 Symbol 注册表里查找指定 Symbol 对应的 key。因此对于非全局 Symbol 它是不起作用的,并且会返回 undefiend。
例如:
alert( Symbol.keyFor(Symbol.for("name")) ); // name, 全局 symbol
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 主要有两个使用场景:
- 作为对象的“隐藏”属性。如果我们为一个对象添加的属性名在另一个脚本里也“存在”的话,我们就可以用一个 Symbol 值来作为属性名创建属性。Symbol 属性不会出现在 for…in 循环遍历中,所以它不会偶尔被列出。它越不能直接访问,因为另一个脚本里没有我们的 Symbol 值,因此,它不会偶尔干预它的行动。
我们可以秘密地使用 Symbol 类型属性名在对象里隐藏一些属性,这样别人就看不到了。
- JavaScript 提供了许多系统 Symbol 值,通过 Symbol.* 的形式访问。我们可以使用它们来修改一些内置行为。例如,在之后非教程里我们会在可迭代对象中使用 Symbol.iterator,在对象转换到原始类型值的时候使用 Symbol.toPrimitive 等等。
技术上讲,Symbol 值并不能 100% 隐藏。这里提供了一个内置方法 Object.getOwnPropertySymbols(obj) 来获得一个对象身上的所有的 Symbol 属性。Reflect.ownKeys(obj) 方法返回一个对象身上的所有属性(包括 Symbol 属性值)。因此他们并不是真的隐藏了。但是大多数的库、内置的方法和语法结构都遵循一个共同的协议。明确地调用前面提到的方法的人可能很清楚他在做什么。
(完)