2022 年 6 月 27 日

符号类型

根据规范,只有两种原始类型可作为对象属性键

  • 字符串类型,或
  • 符号类型。

否则,如果使用其他类型(例如数字),则会自动转换为字符串。因此,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

在某些编程语言中,如 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 会返回完全相同的符号。

符号有两个主要用例

  1. “隐藏”对象属性。

    如果我们想将一个属性添加到“属于”另一个脚本或库的对象中,我们可以创建一个符号并将其用作属性键。符号属性不会出现在 for..in 中,因此不会与其他属性一起意外处理。此外,它不会被直接访问,因为另一个脚本没有我们的符号。因此,该属性将受到意外使用或覆盖的保护。

    因此,我们可以使用符号属性“秘密地”将某些内容隐藏到我们需要的对象中,但其他人不应该看到。

  2. JavaScript 使用了许多系统符号,这些符号可作为 Symbol.* 访问。我们可以使用它们来改变一些内置行为。例如,在教程的后面,我们将使用 Symbol.iterator 来表示 可迭代对象,使用 Symbol.toPrimitive 来设置 对象到原始类型的转换 等。

从技术上讲,符号并不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj),它允许我们获取所有符号。此外,还有一个名为 Reflect.ownKeys(obj) 的方法,它返回一个对象的所有键,包括符号键。但大多数库、内置函数和语法结构都不使用这些方法。

教程地图

评论

在评论前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将它们包装在 <pre> 标记中,对于 10 行以上 - 使用沙盒 (plnkrjsbincodepen…)