根据规范,只有两种原始类型可作为对象属性键
- 字符串类型,或
- 符号类型。
否则,如果使用其他类型(例如数字),则会自动转换为字符串。因此,obj[1]
与 obj["1"]
相同,obj[true]
与 obj["true"]
相同。
到目前为止,我们只使用了字符串。
现在让我们探索符号,看看它们能为我们做什么。
符号
“符号”表示唯一的标识符。
可以使用 Symbol()
创建此类型的变量
let id = Symbol();
创建时,我们可以给符号一个描述(也称为符号名称),这主要用于调试目的
// id is a symbol with the description "id"
let id = Symbol("id");
符号保证是唯一的。即使我们创建了许多具有完全相同描述的符号,它们也是不同的值。描述只是一个不影响任何内容的标签。
例如,这里有两个具有相同描述的符号 - 它们不相等
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
如果您熟悉 Ruby 或其他也具有某种“符号”的语言 - 请不要误导。JavaScript 符号是不同的。
因此,总结一下,符号是一个具有可选描述的“原始唯一值”。让我们看看可以在哪里使用它们。
JavaScript 中的大多数值都支持隐式转换为字符串。例如,我们可以 alert
几乎任何值,它都会起作用。符号是特殊的。它们不会自动转换。
例如,此 alert
将显示错误
let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string
这是一个防止弄乱的“语言保护”,因为字符串和符号在本质上是不同的,不应该意外地将一个转换为另一个。
如果我们真的想显示一个符号,我们需要显式地对其调用 .toString()
,如下所示
let id = Symbol("id");
alert(id.toString()); // Symbol(id), now it works
或者获取 symbol.description
属性以仅显示描述
let id = Symbol("id");
alert(id.description); // id
“隐藏”属性
符号允许我们创建对象的“隐藏”属性,代码的其他部分无法意外访问或覆盖。
例如,如果我们使用属于第三方代码的 user
对象。我们希望向他们添加标识符。
让我们为它使用一个符号键
let user = { // belongs to another code
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // we can access the data using the symbol as the key
使用 Symbol("id")
比使用字符串 "id"
有什么好处?
由于 user
对象属于另一个代码库,因此向它们添加字段是不安全的,因为我们可能会影响该其他代码库中预定义的行为。但是,无法意外访问符号。第三方代码不会意识到新定义的符号,因此可以安全地将符号添加到 user
对象。
另外,想象另一个脚本想在 user
中有自己的标识符,以达到自己的目的。
然后该脚本可以创建自己的 Symbol("id")
,如下所示
// ...
let id = Symbol("id");
user[id] = "Their id value";
我们的标识符和他们的标识符之间不会发生冲突,因为符号总是不同的,即使它们具有相同的名称。
…但是如果我们使用字符串 "id"
而不是符号来实现相同目的,那么就会发生冲突
let user = { name: "John" };
// Our script uses "id" property
user.id = "Our id value";
// ...Another script also wants "id" for its purposes...
user.id = "Their id value"
// Boom! overwritten by another script!
对象字面量中的符号
如果我们想在对象字面量 {...}
中使用符号,我们需要在符号周围加上方括号。
如下所示
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // not "id": 123
};
这是因为我们需要使用变量 id
中的值作为键,而不是字符串“id”。
符号会被 for…in 跳过
符号属性不会参与 for..in
循环。
例如
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age (no symbols)
// the direct access by the symbol works
alert( "Direct: " + user[id] ); // Direct: 123
Object.keys(user) 也忽略它们。这是“隐藏符号属性”原则的一部分。如果另一个脚本或库循环遍历我们的对象,它不会意外访问符号属性。
相反,Object.assign 复制字符串和符号属性
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
这里没有矛盾。这是设计使然。其思想是,当我们克隆对象或合并对象时,我们通常希望复制所有属性(包括符号,如 id
)。
全局符号
正如我们所见,通常所有符号都是不同的,即使它们具有相同的名称。但有时我们希望同名符号成为同一实体。例如,我们应用程序的不同部分希望访问符号 "id"
,表示完全相同的属性。
为了实现这一点,存在一个全局符号注册表。我们可以在其中创建符号并在以后访问它们,它保证通过相同名称重复访问会返回完全相同的符号。
为了从注册表中读取(如果不存在则创建)符号,请使用 Symbol.for(key)
。
该调用检查全局注册表,如果存在描述为 key
的符号,则返回该符号,否则创建新的符号 Symbol(key)
并通过给定的 key
将其存储在注册表中。
例如
// read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created
// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");
// the same symbol
alert( id === idAgain ); // true
注册表中的符号称为全局符号。如果我们想要一个应用程序范围的符号,可以在代码中的任何地方访问,这就是它们的作用。
在某些编程语言中,如 Ruby,每个名称只有一个符号。
在 JavaScript 中,正如我们所见,对于全局符号来说这是正确的。
Symbol.keyFor
我们已经看到,对于全局符号,Symbol.for(key)
根据名称返回符号。要执行相反的操作,即根据全局符号返回名称,我们可以使用:Symbol.keyFor(sym)
例如
// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// get name by symbol
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor
在内部使用全局符号注册表来查找符号的键。因此它不适用于非全局符号。如果符号不是全局的,它将无法找到它并返回 undefined
。
也就是说,所有符号都有 description
属性。
例如
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
alert( Symbol.keyFor(globalSymbol) ); // name, global symbol
alert( Symbol.keyFor(localSymbol) ); // undefined, not global
alert( localSymbol.description ); // name
系统符号
存在许多 JavaScript 在内部使用的“系统”符号,我们可以使用它们来微调对象的各个方面。
它们在规范中 众所周知符号 表中列出
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
- 等等。
例如,Symbol.toPrimitive
允许我们描述对象到原始类型的转换。我们很快就会看到它的用法。
当我们学习相应的语言特性时,其他符号也会变得熟悉。
总结
Symbol
是唯一标识符的原始类型。
符号使用 Symbol()
调用创建,带有可选描述(名称)。
符号始终是不同的值,即使它们具有相同的名称。如果我们希望同名符号相等,那么我们应该使用全局注册表:Symbol.for(key)
返回(如果需要,则创建)一个全局符号,其 key
为名称。使用相同的 key
多次调用 Symbol.for
会返回完全相同的符号。
符号有两个主要用例
-
“隐藏”对象属性。
如果我们想将一个属性添加到“属于”另一个脚本或库的对象中,我们可以创建一个符号并将其用作属性键。符号属性不会出现在
for..in
中,因此不会与其他属性一起意外处理。此外,它不会被直接访问,因为另一个脚本没有我们的符号。因此,该属性将受到意外使用或覆盖的保护。因此,我们可以使用符号属性“秘密地”将某些内容隐藏到我们需要的对象中,但其他人不应该看到。
-
JavaScript 使用了许多系统符号,这些符号可作为
Symbol.*
访问。我们可以使用它们来改变一些内置行为。例如,在教程的后面,我们将使用Symbol.iterator
来表示 可迭代对象,使用Symbol.toPrimitive
来设置 对象到原始类型的转换 等。
从技术上讲,符号并不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj),它允许我们获取所有符号。此外,还有一个名为 Reflect.ownKeys(obj) 的方法,它返回一个对象的所有键,包括符号键。但大多数库、内置函数和语法结构都不使用这些方法。
评论
<code>
标记,对于多行 - 将它们包装在<pre>
标记中,对于 10 行以上 - 使用沙盒 (plnkr,jsbin,codepen…)