本文详细介绍 JavaScript 中的 Symbol 类型,它是一种表示唯一标识符的基本数据类型。文章解释了如何创建 Symbol、其不可自动转换为字符串的特性,以及如何利用 Symbol 创建对象的“隐藏”属性以避免命名冲突。此外,还涵盖了全局 Symbol 注册表的使用、系统 Symbol 的作用以及获取 Symbol 键的方法。
Symbol 类型概述
Symbol 类型
依据规范,对象属性键仅限字符串或 Symbol 类型,排除 Number 与 Boolean。
此前仅涉及字符串,现探讨 Symbol 带来的优势。
Symbol
"Symbol" 值代表唯一标识符。
通过 Symbol() 函数可创建此类值:
// id 是 symbol 的一个实例化对象
let id = Symbol();
创建时可赋予 Symbol 描述(即 Symbol 名),这对代码调试极具价值:
// 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); // 类型错误:无法将 Symbol 值转换为字符串。
此为防混乱的“语言保护”,因字符串与 Symbol 本质不同,不应意外互转。
若需显示 Symbol,需调用 .toString(),如下所示:
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了
或获取 symbol.description 属性,仅显示描述(description):
let id = Symbol("id");
alert(id.description); // id
利用 Symbol 创建隐藏属性
“隐藏”属性
Symbol 允许创建对象“隐藏”属性,其他代码部分无法意外访问或重写。
例如,若使用属第三方代码的 user 对象,欲添加标识符。
可使用 Symbol 键:
let user = { // 属于另一个代码
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据
在字符串 "id" 上使用 Symbol("id") 有何益处?
因 user 属另一代码,且被用于执行操作,故不应随意添加字段,此举不安全。但 Symbol 不被意外访问,第三方代码不可见,故使用 Symbol 或许无碍。 另假设另一脚本欲在 user 中添加自身标识符以实现目的,可能是另一 JavaScript 库,脚本间互不知情。
该脚本可创建自己的 Symbol("id"),如下:
// ...
let id = Symbol("id");
user[id] = "Their id value";
我们的标识符与他们的标识符间无冲突,因 Symbol 总不同,即便名字相同。
……但若出于同样目的,使用字符串 "id" 而非 symbol,则 会 出现冲突:
let user = { name: "John" };
// 我们的脚本使用了 "id" 属性。
user.id = "Our id value";
// ……另一个脚本也想将 "id" 用于它的目的……
user.id = "Their id value"
// 砰!无意中被另一个脚本重写了 id!
Symbol 在对象字面量中的使用
字面量中的 Symbol
若在对象字面量 {...} 中使用 Symbol,需用方括号括起。
如下所示:
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // 而不是 "id:123"
};
因需变量 id 的值作为键,而非字符串 "id"。
Symbol 与 for..in 循环
Symbol 在 for..in 中会被跳过
Symbolic 属性不参与 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)
// 使用 Symbol 任务直接访问
alert( "Direct: " + user[id] );
Object.keys(user) 亦忽略它们。此为“隐藏符号属性”原则一部分。若其他脚本或库遍历对象,不会意外访问符号属性。
相反,Object.assign 会同时复制字符串和 symbol 属性:
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
此处无矛盾,设计如此。想法是克隆或合并 object 时,通常希望 所有 属性被复制(包括如 id 这样的 Symbol)。
其他类型属性键被强制为字符串:
仅能在对象中使用字符串或 symbol 作为键,其它类型会被转换为字符串。
例如,作为属性键使用时,数字 0 变成了字符串 "0":
let obj = {
0: "test" // 和 "0": "test" 一样
};
// 两个 alert 都访问相同的属性(Number 0 被转换为字符串 "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test(同一个属性)
全局 Symbol 注册表
全局 symbol
如前所述,通常所有 Symbol 均不同,即便名字相同。但有时需名字相同的 Symbol 具有相同实体。例如,应用不同部分欲访问的 Symbol "id" 指代完全相同属性。
为此,存在 全局 Symbol 注册表。可在其中创建 Symbol 并稍后访问,确保每次访问相同名字 Symbol 时,返回相同 Symbol。
要从注册表中读取(不存在则创建)Symbol,请使用 Symbol.for(key)。
该调用检查全局注册表,若存在描述为 key 的 Symbol,则返回该 Symbol,否则创建新 Symbol(Symbol(key)),并通过给定 key 存储于注册表中。
例如:
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 Symbol
alert( id === idAgain ); // true
注册表内 Symbol 被称为 全局 Symbol。若需应用范围内 Symbol,可在代码中随处访问 —— 此即其用途。
这听起来像 Ruby:
在一些编程语言中,例如 Ruby,每个名字都有一个 Symbol。
如前所述,在 JavaScript 中,全局 Symbol 亦如此。
Symbol.keyFor 方法
Symbol.keyFor
对于全局 Symbol,不仅有 Symbol.for(key) 按名字返回 Symbol,还有反向调用:Symbol.keyFor(sym),作用完全相反:通过全局 Symbol 返回名字。
例如:
// 通过 name 获取 Symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 Symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor 内部使用全局 Symbol 注册表查找 Symbol 键。故不适用于非全局 Symbol。若 Symbol 非全局,将无法找到并返回 undefined。
即,任何 Symbol 均具有 description 属性。
例如:
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
alert( Symbol.keyFor(globalSymbol) ); // name,全局 Symbol
alert( Symbol.keyFor(localSymbol) ); // undefined,非全局
alert( localSymbol.description ); // name
系统 Symbol
系统 Symbol
JavaScript 内部有许多“系统” Symbol,可用于微调对象各方面。 它们均列于众所周知 Symbol 表规范中:
- Symbol.hasInstance
- Symbol.isConcatSpreadable
- Symbol.iterator
- Symbol.toPrimitive
- ……等等。
例如,Symbol.toPrimitive 允许我们将对象描述为原始值转换。我们很快将看到其使用。 研究相应语言特征时,我们将逐渐熟悉其他 Symbol。
总结
总结
Symbol 是唯一标识符的基本类型
Symbol 是使用带有可选描述(name)的 Symbol() 调用创建的。
Symbol 总是不同值,即便名字相同。若希望同名 Symbol 相等,应使用全局注册表:Symbol.for(key) 返回(如需则创建)以 key 为名字的全局 Symbol。使用 Symbol.for 多次调用 key 相同 Symbol 时,返回即为同一 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。故并非真正隐藏。但多数库、内置方法和语法结构未使用这些方法。
以上为本文全部内容,希望对大家学习有所帮助,也希望大家多多支持本站。