2022-09-26JavaScript00
请注意,本文编写于 581 天前,最后修改于 581 天前,其中某些信息可能已经过时。

Symbol

ES6 中引入了一种新的基础数据类型:Symbol,这是一种新的基础数据类型(primitive type)。

它的功能类似于一种标识唯一性的 ID。通常情况下,我们可以通过调用 Symbol()函数来创建一个 Symbol 实例:

let s1 = Symbol();

或者,你也可以在调用 Symbol()函数时传入一个可选的字符串参数,相当于给你创建的 Symbol 实例一个描述信息:

let s2 = Symbol("another symbol");

如果用当下比较流行的 TypeScript 的方式来描述这个 Symbol()函数的话,可以表示成:

/**
 *  @param {any} description 描述信息。可以是任何可以被转型成字符串的值,如:字符串、数字、对象、数组等
 */
 function Symbol(description:any):symbol;

由于 Symbol 是一种基础数据类型,所以当我们使用 typeof 去检查它的类型的时候,它会返回一个属于自己的类型 symbol,而不是什么 string、object 之类的:

typeof s1; // 'symbol'

另外,我们需要重点记住的一点是:每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false:

let s1 = Symbol();
let s2 = Symbol("another symbol");
let s3 = Symbol("another symbol");

s1 === s2; //false
s2 === s3; //false

1.1 一些应用场景

  • 场景 1:使用 Symbol 来作为对象属性名(key)

在这之前,我们通常定义或访问对象的属性时都是使用字符串,比如下面的代码:

let obj = {
  abc: 123,
  hello: "world",
};

obj["abc"]; // 123
obj["hello"]; // 'world'

而现在,Symbol 可同样用于对象属性的定义和访问:

const PROP_NAME = Symbol();
const PROP_AGE = Symbol();

let obj = {
  [PROP_NAME]: "yd",
};
obj[PROP_AGE] = 6;

obj[PROP_NAME]; // yd
obj[PROP_AGE]; // 6

随之而来的是另一个非常值得注意的问题:就是当使用了 Symbol 作为对象的属性 key 后,在对该对象进行 key 的枚举时,会有什么不同?在实际应用中,我们经常会需要使用Object.keys()或者for...in来枚举对象的属性名,那在这方面,Symbol 类型的 key 表现的会有什么不同之处呢?来看以下示例代码:

let obj = {
  [Symbol("name")]: "yd",
  age: 6,
  title: "symbol",
};
Object.keys(obj); // ["age","title"]
for (let p in obj) {
  console.log(p); // 分别输出 “age” 和 “title”
}
Object.getOwnPropertyNames(obj); // ["age","title"]

由上代码可知,Symbol 类型的 key 是不能通过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义。

也正因为这样一个特性,当使用 JSON.stringify()将对象转换成 JSON 字符串的时候,Symbol 属性也会被排除在输出内容之外:

JSON.stringify(obj); // {"age":6,"title":"symbol"}

我们可以利用这一特点来更好的设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加优雅。

然而,这样的话,我们就没办法获取以 Symbol 方式定义的对象属性了么?非也。还是会有一些专门针对 Symbol 的 API,比如:

// 使用Object的API
Object.getOwnPropertySymbol(obj); // [Symbol(name)]

// 使用新增的的反射API
Reflect.ownKeys(obj); // [Symbol(name),"age","title"]
  • 场景 2:使用 Symbol 代替常量

看一下下面的代码是不是非常熟悉

const TYPE_AUDIO = "AUDIO";
const TYPE_VIDEO = "VIDEO";
const TYPE_IMAGE = "IMAGE";

function handleFileResource(resource) {
  switch (resource.type) {
    case TYPE_AUDIO:
      playAudio(resource);
      break;
    case TYPE_VIDEO:
      playVideo(resource);
      break;
    case TYPE_IMAGE:
      previewImage(resource);
      break;
    default:
      throw new Error("Unknown type of resource");
  }
}

如上面的代码中那样,我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,我们通常希望这几个常量之间是唯一的关系,为了保证这一点,我们需要为常量赋一个唯一的值(比如这里的'AUDIO'、'VIDEO'、 'IMAGE'),常量少的时候还算好,但是常量一多,你可能还得花点脑子好好为他们取个好点的名字。

现在有了 Symbol,我们大可不必这么麻烦了:

const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();

这样定义,直接就保证了三个常量的值是唯一的了!是不是挺方便的呢。

  • 场景 3:使用 Symbol 定义类的私有属性/方法

我们知道在 JavaScript 中,是没有如 Java 等面向对象语言的访问控制关键字 private 的,类上所有定义的属性或方法都是可公开访问的。因此这对我们进行 API 的设计时造成了一些困扰。

而有了 Symbol 以及模块化机制,类的私有属性和方法才变成可能。例如:

在文件 a.js 中

const PASSWORD = Symbol();

class Login {
  constructor(userName, password) {
    this.userName = userName;
    this[PASSWORD] = password;
  }
  checkPassword(pwd) {
    return this[PASSWORD] === pwd;
  }
}
export default Login;

在文件 b.js 中

import Login from "./a";

const login = new Login("admin", "123456");

login.checkPassword("admin"); // true

login.PASSWORD; // oh!no!
login[PASSWORD]; // oh!no!
login["PASSWORD"]; // oh!no!

由于 Symbol 常量 PASSWORD 被定义在 a.js 所在的模块中,外面的模块获取不到这个 Symbol,也不可能再创建一个一模一样的 Symbol 出来(因为 Symbol 是唯一的),因此这个 PASSWORD 的 Symbol 只能被限制在 a.js 内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。

1.2 注册和获取全局 Symbol

通常情况下,我们在一个浏览器窗口中(window),使用 Symbol()函数来定义 Symbol 实例就足够了。

但是,如果你的应用涉及到多个 window(最典型的就是页面中使用了<iframe>),并需要这些 window 中使用的某些 Symbol 是同一个,那就不能使用Symbol()函数了,因为用它在不同 window 中创建的 Symbol 实例总是唯一的,而我们需要的是在所有这些 window 环境下保持一个共享的 Symbol。这种情况下,我们就需要使用另一个 API 来创建或获取 Symbol,那就是Symbol.for(),它可以注册或获取一个 window 间全局的 Symbol 实例:

let gs1 = Symbol.for("global_symbol_1"); // 注册一个全局Symbol
let gs2 = Symbol.for("global_symbol_2"); // 获取全局Symbol

gs1 === gs2; // true

这样一个 Symbol 不光在单个 window 中是唯一的,在多个相关 window 间也是唯一的了。

1.3 一些需要特别注意的点

  1. Symbol 函数前不能使用 new 命令,否则会报错。
  2. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
  3. Symbol 作为属性名,该属性不会出现在 for...infor...of 循环中,也不会被 Object.keys()Object.getOwnPropertyNames()JSON.stringify() 返回。
  4. Object.getOwnPropertySymbols 方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
  5. Symbol.for 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
  6. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

本文作者:前端小毛

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!