🗽 JavaScript symbol 类型
在 JavaScript 规范中,只有两种原始类型可以作为对象的属性键(key)。
- 字符串 (使用其他的原始类型,例如数字或者布尔类型,都会自动转化为字符串)
- symbol 类型
# symbol 唯一标识符
语法:可以使用 Symbol()
创建 symbol 类型的值,返回给变量一个唯一的标识符的值。在括号中的参数可以是一个描述(描述可以相同)。
🌰 例子: let id = Symbol()
/ let id = Symbol("id")
symbol 保证是唯一的。即使创建了许多具有相同描述的 symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。
🌰 例子:
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
2
3
4
symbol 类型不会被自动转换为字符串。JavaScript 中的大多数值都支持字符串的隐式转换。例如,可以
alert
任何值(自动转换为字符串输出),都可以生效。symbol 比较特殊,它不会被自动转换。🌰 例子:
let id = Symbol("id"); alert(id); // 类型错误:无法将 symbol 值转换为字符串。
1
2可以使用强制转换解决:(将 symbol 转换为字符串)
alert(id.toString())
1或者只显示描述:
et id = Symbol("id"); alert(id.description)
1
2
# symbol 隐藏属性
symbol 允许我们创建对象的「隐藏」属性,代码的任何其他部分都不能意外访问或重写这些属性。
🌰 例子:
let user {
name = 'Simon'
}
let id = Sysbol('id')
user.[id] = 1 // 添加新的属性
alert(user[id])
2
3
4
5
6
7
user
对象属于其他的代码,那些代码也会使用这个对象,所以我们不应该在它上面直接添加任何字段,这样很不安全。但是你添加的 symbol 属性不会被意外访问到,第三方代码根本不会看到它,所以使用 symbol 基本上不会有问题。
🌰 例子:假设另一个脚本希望在 user
中有自己的标识符,以实现自己的目的。这可能是另一个 JavaScript 库,因此脚本之间完全不了解彼此。
然后该脚本可以创建自己的 Symbol("id")
,像这样:
let id = Symbol('id')
user[id] = 'their id value'
2
虽然有相同的名称,但是 symbol 类型保证了唯一性。
如果仅仅使用字符串作为属性名:
let user = { // ... } // 一个脚本中 user.id = 'id1' // 另一个脚本中也添加id,产生冲突 user.id = 'id2'
1
2
3
4
5
6
7
# 在对象字面量中使用 symbol
在对象 { … }
字面量中使用 symbol 时,需要使用方括号把它括起来。
🌰 例子:
let id = Symbol('id')
let user = {
name: 'Simon',
[id]: 123
}
2
3
4
5
这里需要 symbol 类型的
id
作为对象属性的键, 所以需要使用方括号[]
区分开字符串‘id’
与 symbol。
# 在遍历循环中的 symbol
⭐️ symbol 属性不参与 for..in
循环。(同理还有 Object.keys()
)这是一般「隐藏符号属性」原则的一部分,如果另一个脚本或库遍历我们的对象,它不会意外地访问到符号属性。
🌰 例子:
let id = Symbol('id')
let user = {
name: 'Simon',
[id]: 123
}
for (let key in user) alert(key) // 只有name
alert(user[id]) // 单独可以直接访问
2
3
4
5
6
7
8
9
但是在 Object.assign()
中,会同时复制字符串和 symbol 属性。
🌰 例子:
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
2
3
4
5
6
7
8
# 全局 symbol
如前所述,通常所有的 symbol 都是不同的,即使它们有相同的名字。但有时想要名字相同的 symbol 具有相同的实体时,例如,应用程序的不同部分想要访问的 symbol "id"
指的是完全相同的属性。
为了实现这一点,可以使用 全局 symbol 注册表。可以在其中创建 symbol 并在稍后访问它们,它可以确保每次访问相同名字的 symbol 时,返回的都是相同的 symbol。
# Symbol.for(key)
从注册表读取(不存在则创建) symbol ,使用
Symbol.for(key)
(该调用会检查全局注册表,如果有一个描述为
key
的 symbol,则返回该 symbol,否则将创建一个新 symbol(Symbol(key)
),并通过给定的key
将其存储在注册表中。)注册表中的 symbol 被称为 全局 symbol。如果我们想要一个应用程序范围内的 symbol,可以在代码中随处访问 —— 这就是它们的用途。
🌰 例子:
let id = Symbol.for('id')
let idAgain = Symbol.for('id')
alert(id === idAgain) // true
2
3
4
# Symbol.keyFor
Symbol.keyFor(symbol)
,它的作用与Symbol.for(key)
完全反过来:通过全局 symbol 返回一个名字。作用是查找 symbol 的键。所以它不适用于非全局 symbol。如果 symbol 不是全局的,它将无法找到它并返回
undefined
。任何 symbol 都具有
description
属性。
🌰 例子:
// 通过 name 获取 symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
2
3
4
5
6
7
🌰 例子:(非全局 symbol)
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
alert( Symbol.keyFor(globalSymbol) ); // name,全局 symbol
alert( Symbol.keyFor(localSymbol) ); // undefined,非全局
alert( localSymbol.description ); // name
2
3
4
5
6
7
# JavaScript 系统 symbol
avaScript 内部有很多「系统 symbol」,可以使用它们来微调对象的各个方面。它们都被列在了 众所周知的 symbol (opens new window) 表的规范中:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
- …… 等等。
# symbol 总结
symbol
是 唯一标识符的基本类型,使用带有可选描述(name)的Symbol()
调用创建的。- symbol 总是 不同的值,即使它们有相同的名字。如果希望同名的 symbol 相等,那么应该使用全局注册表:
Symbol.for(key)
返回(如果需要的话则创建)一个以key
作为名字的全局 symbol。使用Symbol.for
多次调用key
相同的 symbol 时,返回的就是同一个 symbol。 - symbol 不是 100% 隐藏的。有一个内建方法 Object.getOwnPropertySymbols(obj) (opens new window) 允许我们获取所有的 symbol。还有一个名为 Reflect.ownKeys(obj) (opens new window) 的方法可以返回一个对象的 所有 键,包括 symbol。所以它们并不是真正的隐藏。
使用 symbol 的场景:
- 隐藏对象属性。
- JavaScript 使用了许多系统 symbol,这些 symbol 可以作为
Symbol.*
访问。可以使用它们来改变一些内建行为。例如,使用Symbol.iterator
来进行 迭代 (opens new window) 操作,使用Symbol.toPrimitive
来设置 对象原始值的转换 (opens new window) 等等。