你知道 JavaScript Symbol 类型是什么,怎么用吗?

Symbol 类型

根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。不是 Number,也不是 Boolean,只有字符串或 Symbol 这两种类型。javascript

到目前为止,咱们只见过字符串。如今咱们来看看 Symbol 能给咱们带来什么好处。html

Symbol

"Symbol" 值表示惟一的标识符。java

可使用 Symbol() 来建立这种类型的值:react

// id 是 symbol 的一个实例化对象
let id = Symbol();
复制代码

建立时,咱们能够给 Symbol 一个描述(也称为 Symbol 名),这在代码调试时很是有用:git

// id 是描述为 "id" 的 Symbol
let id = Symbol("id");
复制代码

Symbol 保证是惟一的。即便咱们建立了许多具备相同描述的 Symbol,它们的值也是不一样。描述只是一个标签,不影响任何东西。github

例如,这里有两个描述相同的 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 容许咱们建立对象的“隐藏”属性,代码的任何其余部分都不能意外访问或重写这些属性。

例如,若是咱们使用的是属于第三方代码的 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,则须要使用方括号把它括起来。

就像这样:

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // 而不是 "id:123"
};
复制代码

这是由于咱们须要变量 id 的值做为键,而不是字符串 "id"。

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 "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,不只有 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

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 有两个主要的使用场景:

  1. “隐藏” 对象属性。 若是咱们想要向“属于”另外一个脚本或者库的对象添加一个属性,咱们能够建立一个 Symbol 并使用它做为属性的键。Symbol 属性不会出如今 for..in 中,所以它不会意外地被与其余属性一块儿处理。而且,它不会被直接访问,由于另外一个脚本没有咱们的 symbol。所以,该属性将受到保护,防止被意外使用或重写。

    所以咱们可使用 Symbol 属性“秘密地”将一些东西隐藏到咱们须要的对象中,但其余地方看不到它。

  2. JavaScript 使用了许多系统 Symbol,这些 Symbol 能够做为 Symbol.* 访问。咱们可使用它们来改变一些内置行为。例如,在本教程的后面部分,咱们将使用 Symbol.iterator 来进行 迭代 操做,使用 Symbol.toPrimitive 来设置 对象原始值的转换 等等。

从技术上说,Symbol 不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj) 容许咱们获取全部的 Symbol。还有一个名为 Reflect.ownKeys(obj) 的方法能够返回一个对象的 全部 键,包括 Symbol。因此它们并非真正的隐藏。可是大多数库、内置方法和语法结构都没有使用这些方法。

本文首发于微信公众号「技术漫谈」,欢迎微信搜索关注,订阅更多精彩内容。


现代 JavaScript 教程:开源的现代 JavaScript 从入门到进阶的优质教程。React 官方文档推荐,与 MDN 并列的 JavaScript 学习教程

在线免费阅读:zh.javascript.info


扫描下方二维码,关注微信公众号「技术漫谈」,订阅更多精彩内容。

相关文章
相关标签/搜索