# 7. Symbol

## 1. 通用知识

在计算机编程中，Symbol 是一种原始数据类型，它的实例具有唯一的人类可读形式。Symbol（符号）可以用作标识符，在一些编程语言中也被称为 Atom（原子）。通过把它们都保存在 Symbol 表中来确保其唯一性。

Symbol 最常见的用法是执行反射，尤其是回调。最常见的间接用途是创建对象链接，在不同范围内或在同一范围内多次声明的标识符可以通过称为链接的过程来引用相同的对象或函数。在编程语言中，尤其是像 C 和 C++ 这样的编译语言，链接描述了标识符如何在整个程序或单个翻译单元中指向同一个实体。

在最简单的实现中，Symbol 本质上是命名整数，例如 C 语言中的枚举类型。

## 2. Symbol 类型

在 JavaScript 中，Symbol 是一个原始类型，它所表示的值是唯一的、不可变的。

> A Symbol is a unique and immutable primitive value.

Symbol 通常用于向对象添加唯一的属性 key，该键 key 不会和其他代码可能添加到该对象的键发生冲突。并且这些 Symbol 键对于访问该对象的常规方法都是隐藏的，这也在一定程度上实现了弱封装或弱信息隐藏。

数据类型是 Symbol 的值，就可以称为 Symbol 原始值，或 Symbol 值，或简称为 Symbol。

```javascript
let sym = Symbol('sym');
typeof sym; // 'symbol'
```

### 2.1 唯一性

每个 `Symbol()` 调用都保证返回一个唯一的 Symbol。比如：

```javascript
// 确保唯一
const s1 = Symbol();
const s2 = Symbol('hi');
const s3 = Symbol('hi');
s2 === s3; // false, 即便参数都是 'hi'
```

每个 `Symbol.for('key')` 调用都将始终返回相同的 Symbol，它会先在全局 Symbol 注册表中查找给定键值的 Symbol。如果找到了就返回，否则就新建一个再返回，同时将其添加到全局 Symbol 注册表中的给定键下。比如：

```javascript
const s1 = Symbol.for('hi');
const s2 = Symbol.for('hi');
s1 == s2; // true
s1 === s2; // true
```

用 `Symbol()` 函数创建的 Symbol，其值在程序的整个生命周期内保持唯一。如果要创建跨文件甚至是跨领域（realm，每个领域都有自己的全局范围）可用的共享 Symbol，就得使用方法 `Symbol.for()` 和 `Symbol.keyFor()` 了，它们是在全局 Symbol 注册表里设置和检索的。

### 2.2 全局注册表

全局 Symbol 注册表是个虚构概念，在 JavaScript 引擎中它可能不对应任何内部数据结构——即使存在这样的注册表，它的内容也没法用 JavaScript 代码获取，除非使用 `Symbol.for()` 和 `Symbol.keyFor()`。

1. `Symbol.for(tokenString)` 参数是字符串，返回值是 Symbol 原始值
2. `Symbol.keyFor(symbolValue)` 参数是 Symbol 原始值，返回值是与其对应的字符串 key

这两个方法是互逆的，所以以下代码都返回 true：

```javascript
const s1 = Symbol.for('hi');
Symbol.keyFor(s1) === 'hi'; // true
Symbol.for(Symbol.keyFor(s1)) === s1; // true
```

## 3. Symbol 对象

### 3.1 构造器

内置对象 Symbol 的构造器返回一个 Symbol 原始值，该值一定是唯一的。

与其它原始值的对象包装器不同，`Symbol()` 只能用作函数形式不能使用 `new`，而且当函数用时并不做类型转换。如下：

1. 函数 `Symbol()` 不做类型转换，而是每次都创建一个全新的 Symbol。比如 `Symbol('hi')` 是不会将字符串 `'hi'` 强制转换为 Symbol 类型的。
2. 不支持 `new` 语法。当和操作符 `new` 配合使用时，会报错，这么做是为了防止开发者创建显式的 Symbol 包装器对象而不是全新的 Symbol 原始值。

```javascript
// 不支持 new 
const sym = new Symbol(); // TypeError: Symbol is not a constructor
```

如果真的是要创建一个 Symbol 包装器对象，可以使用 `Object()` 方法。代码如下：

```javascript
const sym = Symbol();
const symObj = Object(sym);
typeof sym;    // 'symbol'
typeof symObj; // 'object'
```

### 3.2 静态属性

#### 3.2.1 Well-known Symbols

Symbol 对象的所有静态属性都是 Symbol 本身，其值在各个领域（realms）中都是恒定的。它们被称为 Well-known Symbols（众所周知的符号），目的是充当某些内置 JavaScript 操作的“协议”，允许用户去自定义一些语言的行为。比如：

```javascript
// 创建了一个新对象 o，并给它了一个属性 Symbol.toStringTag
let o = { [Symbol.toStringTag]: "MyObject" };
// 然后用字符串加法触发了 Object.prototype.toString 的调用
console.log(o + ""); // [object MyObject]
```

在 Well-known Symbols 出现之前，JavaScript 使用普通属性来实现某些内置操作。比如 `JSON.stringify()` 函数会尝试调用每个对象的 `toJSON()` 方法，比如 `String()` 函数会调用对象的 `toString()` 和 `valueOf()` 方法。然而，随着语言添加了越来越多的操作，将每个新操作都指定为“神奇属性”，就有可能破坏向后兼容性，也会让语言的行为变得更难推理。Well-known Symbols 机制，允许我们自定义对普通代码不可见的行为，通常是仅仅读取字符串属性。

所以，在接下来介绍 Symbol 静态属性的时候，虽然我们会说“Symbol.xxx 是一种...的方法”，但需要知道的是，这是将其作为对象的方法名使用时的语义，而不是指 Symbol 值本身的含义。

#### 3.2.2 静态属性列表

1. 常规功能
   1. `Symbol.toPrimitive`，将对象转换为相应的原始值
   2. `Symbol.toStringTag`，对象的默认字符串描述，通过内置方法 `Object.prototype.toString()` 访问
   3. `Symbol.hasInstance`，判断对象是否是构造器的实例。由 `instanceof` 运算符调用
   4. `Symbol.species`，用于创建派生对象的构造函数
   5. `Symbol.unscopables`，一个对象值，其自身和继承的属性名被排除在关联对象的 with 环境绑定之外
2. 迭代器相关：数组和类数组
   1. `Symbol.iterator`，返回对象的默认 iterator。在 `for-of` 语句中使用
   2. `Symbol.asyncIterator`，返回对象的默认 AsyncIterator。在 `for-await-of` 语句中使用
3. 用于数组对象
   1. `Symbol.isConcatSpreadable` 布尔值，是否应将对象展平为数组元素。由 `Array.prototype.concat()` 使用
4. 用于字符串对象
   1. `Symbol.search`，由 `String.prototype.search()` 使用
   2. `Symbol.replace`，由 `String.prototype.replace()` 使用
   3. `Symbol.split`，由 `String.prototype.split()` 使用
   4. `Symbol.match`，由 `String.prototype.match()` 使用
   5. `Symbol.matchAll`，由 `String.prototype.matchAll()` 使用

### 3.3 静态方法

这两个都是从[全局 Symbol 注册表](#2.2-quan-ju-zhu-ce-biao)中设置或检索的。

1. `Symbol.for(key)`：始终返回相同 key 的 Symbol，若没找到就新建一个
2. `Symbol.keyFor(sym)`：返回共享 Symbol 的 key

### 3.4 实例属性

1. `Symbol.prototype.description`，只读字符串

### 3.5 实例方法

1. `Symbol.prototype.valueOf()`
2. `Symbol.prototype.toString()`
3. `Symbol.prototype[@@toPrimitive]`

## 4. 注意事项

### 4.1 获取 Symbol 属性

获取对象属性的相关方法，可查阅[对象的属性描述符](https://anjia1.gitbook.io/web/js/object/property-descriptors#2.1-enumerable)。&#x20;

1. String 属性和 Symbol 属性
   1. `Object.prototype.propertyIsEnumerable()`
   2. `Object.prototype.hasOwnProperty()`
   3. `in` 操作符
   4. `Object.getOwnPropertyDescriptors()`
   5. `Reflect.ownKeys()`
2. 仅 String 属性
   1. `Object.getOwnPropertyNames()`
   2. `Object.keys()`
   3. `for..in` 语句
3. 仅 Symbol 属性
   1. `Object.getOwnPropertySymbols()`

### 4.2 `JSON.stringify()`

当使用 `JSON.stringify()` 函数时，Symbol 键属性会被完全忽略。

### 4.3 Symbol 类型转换

1. 当松散比较 `==` 时，Symbol 对象和 Symbol 原始值是相等的
2. 当 Symbol 对象作为对象的属性 key 时，该对象会被强制转换成 Symbol 原始值
3. 不能将 Symbol 转换成 String 或者 Number 类型

```javascript
// 1. 对象和原始值，松散相等
let sym = Symbol('hello');
Object(sym) == sym;  // true, 松散相等
Object(sym) === sym; // false，全等不等

// 2. 当作为属性的key时，对象会被强制转换成原始值
let obj = {};
obj[sym] = 'world';
obj[Object(sym)] = 'new world';
obj[sym]; // 'new world'

// 3. 报错：不能转成字符串和数值
Symbol('hello') + 'world'; // TypeError: Cannot convert a Symbol value to a string
+sym; // Uncaught TypeError: Cannot convert a Symbol value to a number
sym | 0; // Uncaught TypeError: Cannot convert a Symbol value to a number
```

## 5. 主要参考

* <https://en.wikipedia.org/wiki/Symbol_(programming)>
* <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol>
* <https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-symbol-objects>
