JavaScript Symbol 类型详解:唯一标识符与隐藏属性

作者:闭包旅人时间:2026-10-24 03:17:24浏览:89

本文详细介绍 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。故并非真正隐藏。但多数库、内置方法和语法结构未使用这些方法。

以上为本文全部内容,希望对大家学习有所帮助,也希望大家多多支持本站。

作者最新文章