在 TypeScript 中,Symbol 是 ES6 引入的一种原始数据类型,与 number、string、boolean 等基础类型并列。它表示一个独一无二、不可变的值,通常被用作对象属性的键,以确保属性名不会发生冲突。
在 TypeScript 中声明一个 Symbol 类型的变量,写法如下:
typescript
let sym: symbol = Symbol("描述信息");
这里的 "描述信息" 只是一个可选的描述字符串,仅用于调试或日志输出,并不会影响 Symbol 本身的唯一性。即使两个 Symbol 使用相同的描述,它们也是完全不同的两个值。
1. Symbol 具有怎样的特性(举例)
Symbol 的核心特性可以概括为以下几点:
1.1 唯一性
每次调用 Symbol() 都会生成一个全新的、独一无二的值,即使描述相同也不相等:
typescript
const s1 = Symbol("key");
const s2 = Symbol("key");
console.log(s1 === s2); // false
1.2 不可变性
Symbol 一旦创建,其值就无法被修改,它本身是一个原始值,不是对象,因此也不能调用 new Symbol():
typescript
// 错误写法:Symbol 是原始类型,不能使用 new
// const s = new Symbol();
// 正确写法
const s = Symbol("ok");
1.3 可作为对象属性的键
Symbol 最常见的用途是作为对象属性的键,从而避免属性名冲突:
typescript
const uniqueKey = Symbol("id");
const obj = {
[uniqueKey]: 123,
name: "Tom"
};
console.log(obj[uniqueKey]); // 123
1.4 不会被常规遍历方法枚举
使用 for...in 或 Object.keys() 遍历对象时,Symbol 类型的属性键不会被枚举出来:
typescript
const key = Symbol("hidden");
const obj = { name: "Tom", [key]: "secret" };
console.log(Object.keys(obj)); // ["name"]
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(hidden) ]
2. unique Symbol 如何正确理解
在 TypeScript 中,如果仅仅把变量推导为 symbol 类型,类型系统是无法分清具体的 Symbol 的。比如在如下的例子中,在 TypeScript 看来,foo 和 bar 都是 Symbol 类型,它可以让你写 bar = foo,但这在逻辑上可能是不合理的。
typescript
let foo: symbol = Symbol("Foo");
let bar : symbol = Symbol("bar");
bar= foo;
为了解决这个问题,TypeScript 引入了 unique symbol。它是 symbol 的子类型(Subtype),代表某一个特定 、具体 、独一无二 的 Symbol 类型。严格的使用限制因为 unique symbol 代表一个具体的、不可变的值,所以 TypeScript 对它有极其严格的语法要求:必须使用 const 声明(不能用 let 或 var),或者作为类(Class)的 readonly static 属性。不能手动显式指定类型为 unique symbol 却赋值一个普通变量,它只能由 TypeScript 通过 const 自动推导出来。
typescript
const foo: unique symbol = Symbol("Foo") ; // OK
/// (TS) A variable whose type is a 'unique symbol' type must be 'const'.
let bar: unique symbol = Symbol("Bar") ;
// (TS) A variable whose type is a 'unique symbol' type must be 'const'.
var baz : unique symbol = Symbol("Baz") ;
class cls {
// OK
static readonly foo: unique symbol = Symbol("Bar") ;
// (TS) A property of a class whose type is a 'unique symbol' type must be both 'static' and 'readonly'.
static bar: unique symbol = Symbol("Bar") ;
}
3. Symbol 的具体应用场景
Symbol 是 TypeScript 中一个非常特殊的原始类型,它的核心价值在于唯一性 。无论是作为对象属性的键来避免冲突,还是借助 unique symbol 在类型层面区分不同的标记,Symbol 都为开发者提供了一种更安全、更灵活的设计手段。在实际项目中,合理使用 Symbol 可以让代码结构更清晰,也能减少因命名冲突而引发的潜在问题。
3.1 防止对象属性名冲突
当多个模块或库需要往同一个对象上挂载属性时,使用 Symbol 作为键可以避免命名冲突:
typescript
const libA = Symbol("libA");
const libB = Symbol("libB");
const sharedObj: any = {};
sharedObj[libA] = "A 库的数据";
sharedObj[libB] = "B 库的数据";
console.log(sharedObj[libA]); // A 库的数据
console.log(sharedObj[libB]); // B 库的数据
3.2 定义类的"隐藏"成员
利用 Symbol 属性不会被常规方法枚举的特性,可以在类中定义一些内部使用的成员:
typescript
const _count = Symbol("count");
class Counter {
private [_count]: number = 0;
increment() {
this[_count]++;
}
get value() {
return this[_count];
}
}
const c = new Counter();
c.increment();
c.increment();
console.log(c.value); // 2
3.3 实现全局注册表(Symbol.for)
Symbol.for() 会在全局注册表中查找并复用已存在的 Symbol,适合在多个模块间共享同一个 Symbol:
typescript
const s1 = Symbol.for("shared");
const s2 = Symbol.for("shared");
console.log(s1 === s2); // true
4. 使用内置的 Symbol 常量
JavaScript 提供了一些内置的 Symbol 常量,用于自定义对象的基础行为。
4.1 Symbol.iterator & Symbol.asyncIterator
Symbol.iterator和Symbol.asyncIterator分别用于定义一个同步和异步迭代器, 前者会在对象被用于 for...of 循环、解构赋值(... 展开运算符)或 Array.from() 时被调用, 后者则在for await...of 循环遍历异步数据源被调用。
typescript
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let current = this.from;
return {
next: () => current <= this.to ? { value: current++, done: false } : { done: true }
};
}
};
for (let num of range) console.log(num); // 输出: 1, 2, 3
4.2 Symbol.toPrimitive
Symbol.toPrimitive用于定义当前对象针对指定基元类型(数字、字符串、布尔值)的转换规则。自定义对象在被转换为原始类型值时的行为。对象参与数学运算(如 +、-)或字符串拼接、比较时会被调用。在下面的演示程序中,我们定义了常量对象PI,并利用将此Symbol作为属性名称定义了类型转换函数。
typescript
const PI = {
[Symbol.toPrimitive](hint:string) {
if (hint === 'number') return 3.14;
if (hint === 'string') return 'π';
return true; // 默认
}
};
console.log(`${PI}`); //'π'
console.log(PI as any * Math.pow(2,2)); // 12.56
4.3 Symbol.hasInstance
Symbol.hasInstance用来自定义 instanceof 运算符的识别逻辑。当执行 obj instanceof MyClass 时,实际上是在调用 MyClass[Symbol.hasInstance](obj)。在下面的演示程序中,我们定义了表示质数的类型PrimeNumber,并以此Symbol命名了一个静态方法用来确定指定的对象是否是一个质数。
typescript
class PrimeNumber
{
static [Symbol.hasInstance](num:any)
{
return PrimeNumber.isPrimeNumber(num)
}
static isPrimeNumber(num:any)
{
if (num <= 1 || !Number.isInteger(num)) return false;
if (num === 2) return true;
if (num % 2 === 0) return false;
const boundary = Math.sqrt(num);
for (let i = 3; i <= boundary; i += 2)
{
if (num % i === 0) return false;
}
return true;
}
}
console.log(2 as any instanceof PrimeNumber) // True
console.log(3 as any instanceof PrimeNumber) // True
console.log(4 as any instanceof PrimeNumber) // False
4.4 其它
TypeScript还为很多常用的应用场景定义了对应的Symbol, 比如当你在字符串上调用 match、replace、search、split 等方法,且传入的参数是一个自定义对象时,你可以通过以下符号拦截这些操作:
- Symbol.match :改变
String.prototype.match()的行为。也用于 isRegExp 的内部判断; - Symbol.replace :改变
String.prototype.replace()的行为,允许自定义替换逻辑; - Symbol.search :改变
String.prototype.search()的行为; - Symbol.split :改变
String.prototype.split()的行为。