Mr. Panda
Tech For Fun

[JavaScript] JavaScript Symbol 详解

这篇文章对 javascript 中 symbol 进行了详细的解读,内容包含 symbol 的含义、语法、应用场景、内置 symbol、typescript 中的 symbol 等内容。文章重点是 symbol 的使用场景、symbol 的内置对象的使用、symbol 在 typescript 中的类型推断等。建议结合参考资料学习和理解。

什么是 Symbol?

symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

Symbol 的应用场景

创建匿名的对象属性

数据类型 “symbol” 是一种基本数据类型,该类型的性质在于这个类型的值可以用来创建匿名的对象属性。该数据类型通常被用作一个对象属性的键值——当你想让它是私有的时候。例如,symbol 类型的键存在于各种内置的 JavaScript 对象中。同样,自定义类也可以这样创建私有成员。

当一个 symbol 类型的值在属性赋值语句中被用作标识符,该属性(像这个 symbol 一样)是匿名的;并且是不可枚举的。因为这个属性是不可枚举的,它不会在循环结构 “for( ... in ...)” 中作为成员出现,也因为这个属性是匿名的,它同样不会出现在 “Object.getOwnPropertyNames()” 的返回数组里。这个属性可以通过创建时的原始 symbol 值访问到,或者通过遍历 “Object.getOwnPropertySymbols()” 返回的数组。

为什么字符串对象属性不是匿名的? 

传统方式下以字符串作为对象属性的键。这样,只要能得到这个字符串在内存中的地址,就可以访问该属性。由于同一个字符串只会在常量区生成一次,因此在任何时候通过“getName”这个字符串得到的内存中的地址是一样的。

消除“魔术字符串”

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

const shapeType = {
  triangle: Symbol(),
};

switch (shape) {
  case shapeType.triangle: //消除了一个魔术字符串
    area = 0.5 * options.width * options.height;
    break;
  /* ... more code ... */
}

Singleton 模式(单例模式)

单例模式中往往将实例挂载到 globalThis(web 中是 window,node 中是 global) 上,如果只是普通的 key,可能会有被覆盖的风险,使用 Symbol 作为 key 可以保证实例不会被覆盖,同时可以保证 key 值在 globalThis 上的私有性,防止外界篡改。

// mod.js
const FOO_KEY = Symbol.for("foo");

function A() {
  this.foo = "hello";
}

if (!global[FOO_KEY]) {
  global[FOO_KEY] = new A();
}

module.exports = global[FOO_KEY];

Symbol 的内置值

除了用户定义的symbols,还有一些内置symbols。 内置symbols用来表示语言内部的行为。

以下为这些symbols的列表:

  • Symbol.hasInstance 方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。
  • Symbol.isConcatSpreadable 布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。
  • Symbol.iterator 方法,被for-of语句调用。返回对象的默认迭代器。
  • Symbol.match 方法,被String.prototype.match调用。正则表达式用来匹配字符串。
  • Symbol.replace 方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。
  • Symbol.search 方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。
  • Symbol.species 函数值,为一个构造函数。用来创建派生对象。
  • Symbol.split 方法,被String.prototype.split调用。正则表达式来用分割字符串。
  • Symbol.toPrimitive 方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。
  • Symbol.toStringTag 方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。
  • Symbol.unscopables 对象,它自己拥有的属性会被with作用域排除在外。

下面将详细讲解这些内置的 Symbols。

参见:阮一峰:ECMAScript 6 入门 - Symbol

Symbol.toPrimitive 和 Symbol.toStringTag

Symbol.toPrimitive 定义一个对象怎么转化为原始类型,Symbol.toStringTag 定义对象怎么实现 toString 方法。下面的代码自定义对象类型,并且自定义对象转化为原始类型的方法和 toString 方法。

class Item {
   #item;
 constructor(item) {
     if (item?.constructor !== Number) throw new TypeError();
     this.#item = item.valueOf();
   }
 Symbol.toPrimitive {
     // hint can be "number", "string", and "default" 
     switch (hint) {
       case 'number': 
         return this.#item;
       case 'string': 
       case 'default': 
         return Item: ${this.#item};
       default:
         return null;
     }
   }
 get [Symbol.toStringTag]() {
     return this.constructor.name;
   }
 }
 const item = new Item(42);
 console.log(Number(item));
 console.log(String(item));
 console.log(item.toString());
 console.log(item);
 /* Output:
 42
 Item: 42
 [object Item]
 Item {}
 */

Symbol.for() 和 Symbol.keyFor() 

Symbol.for()

如果使用同一个 Symbol 值,可以使用Symbol.for()方法。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

Symbol.for("bar") === Symbol.for("bar"); // true
Symbol("bar") === Symbol("bar"); // false

Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。

Symbol.keyFor()

Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key

let s1 = Symbol.for("foo");
Symbol.keyFor(s1); // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2); // undefined

由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。

Symbols in TypeScript

symbol 在 typescript 中的类型是 symbol,子类型 unique symbol 与具体的 symbol 强关联,通过 typeof 可以获取 unique symbol 的类型。

const PROD: unique symbol = Symbol("Production mode");
const DEV: unique symbol = Symbol("Development mode");

function showWarning(msg: string, mode: typeof DEV | typeof PROD) {
  // ...
}

运行时枚举(Runtime Enums)

symbols 可以重建 enum 在运行时的行为。enums 在 typescript 中是不透明的,因此不能给 enums 设置 string 值,因为 ts 会将这些值视为独一无二的。

enum Colors {
  Red = "Red",
  Green = "Green",
  Blue = "Blue",
}

const c1: Colors = Colors.Red;
const c2: Colors = "Red"; // 💣 No direct assigment possible

enum Moods {
  Happy = "Happy",
  Blue = "Blue",
}

// 💣 This condition will always return 'false' since the
// types 'Moods.Blue' and 'Colors.Blue' have no overlap.
if (Moods.Blue === Colors.Blue) {
  // Nope
}

尽管是值的类型相同,ts 将之视为不同且不可比较。

在 js 中,可以通过 symbols 创建 enum。

// All Color symbols
const COLOR_RED: unique symbol = Symbol("RED");
const COLOR_ORANGE: unique symbol = Symbol("ORANGE");
const COLOR_YELLOW: unique symbol = Symbol("YELLOW");
const COLOR_GREEN: unique symbol = Symbol("GREEN");
const COLOR_BLUE: unique symbol = Symbol("BLUE");
const COLOR_INDIGO: unique symbol = Symbol("INDIGO");
const COLOR_VIOLET: unique symbol = Symbol("VIOLET");
const COLOR_BLACK: unique symbol = Symbol("BLACK");

// All colors except Black
const Colors = {
  COLOR_RED,
  COLOR_ORANGE,
  COLOR_YELLOW,
  COLOR_GREEN,
  COLOR_BLUE,
  COLOR_INDIGO,
  COLOR_VIOLET,
} as const;

function getHexValue(color) {
  switch (color) {
    case Colors.COLOR_RED:
      return "#ff0000";
    //...
  }
}

这些 ts 类型标注需要注意:

  1. symbols 标注为 unique symbol,表示不可修改。
  2. enum 标注为 const,表示在 enum 中定义的 symbols 将会被 ts 保护。ts 的推断范围将会从 symbol 限制为具体的 symbol 类型。

可以通过函数类型声明更加安全的获取 enum 中 symbol 的类型。

type ValuesWithKeys<T, K extends keyof T> = T[K];
type Values<T> = ValuesWithKeys<T, keyof T>;

function getHexValue(color: Values<typeof Colors>) {
  switch (color) {
    case COLOR_RED:
    // super fine, is in our type
    case Colors.COLOR_BLUE:
      // also super fine, is in our type
      break;
    case COLOR_BLACK:
      // what? What is this??? TypeScript errors 💥
      break;
  }
}

如果使用了 symbols keys 而不是仅仅使用 symbols values,就不用使用上述的类型 helper 和 const context。

const ColorEnum = {
  [COLOR_RED]: COLOR_RED,
  [COLOR_YELLOW]: COLOR_YELLOW,
  [COLOR_ORANGE]: COLOR_ORANGE,
  [COLOR_GREEN]: COLOR_GREEN,
  [COLOR_BLUE]: COLOR_BLUE,
  [COLOR_INDIGO]: COLOR_INDIGO,
  [COLOR_VIOLET]: COLOR_VIOLET,
};

function getHexValueWithSymbolKeys(color: keyof typeof ColorEnum) {
  switch (color) {
    case ColorEnum[COLOR_BLUE]:
      // 👍
      break;
    case COLOR_RED:
      // 👍
      break;
    case COLOR_BLACK:
      // 💥
      break;
  }
}

参考链接

Jonsam

一个理科IT宅男,喜欢旅游、分享和美食,做点想做的事情,遇见想见的人。

🍒 美食 | 🌐 FE | 🕌 旅行 | 💻 加班 | ♍ 处女座

jonsam ng

jonsam ng

文章作者

海阔凭鱼跃,天高任鸟飞。

[JavaScript] JavaScript Symbol 详解
这篇文章对 javascript 中 symbol 进行了详细的解读,内容包含 symbol 的含义、语法、应用场景、内置 symbol、typescript 中的 symbol 等内容。文章重点是 symbol 的使用场景、symbol …
扫描二维码继续阅读
2021-09-11