Symbol 是保证唯一的值
Symbol 是 JavaScript 的原始类型之一,和 string、number、boolean、null、undefined、bigint 并列。它最关键的特性很简单:你创建的每个 Symbol 都和其他所有 Symbol 不同,永远如此。
两个 Symbol 都是不带参数创建的,但它们仍然不相等。这不是意外,而正是 Symbol 存在的意义。你无法伪造一个 Symbol,不会意外地重新创建出同一个,也不会和别人的 Symbol 冲突。
你可以给 Symbol 加一段描述,方便调试。描述不影响它的身份:
描述相同,Symbol 却不同。描述只是一个标签,给阅读日志的人看。
为什么需要 Symbol:不会冲突的键
Symbol 存在的主要原因,是让你给对象添加属性时不必担心键和别人的重名。字符串键都挤在同一个扁平的命名空间里:如果两个库都决定把元数据存到 obj.meta,它们就会互相覆盖。Symbol 键不存在这种竞争,因为别人手里没有你的 Symbol。
[ID]: 42 里的方括号是计算属性名语法,意思是“把 ID 的值当作键”。只有持有同一个 ID Symbol 的代码才能读取或覆盖这个属性。其他模块用字符串 "id" 或者自己的 Symbol("id"),指向的都是完全不同的位置。
Symbol 键(基本上)是隐藏的
以 Symbol 为键的属性不会出现在 for...in、Object.keys 或 JSON.stringify 的结果里。它们并不是秘密,有心的代码依然能找到,但在普通的遍历中它们不会碍事。
Object.keys 和 JSON.stringify 完全跳过了 Symbol 键。如果你确实需要找出 Symbol 属性,可以用 Object.getOwnPropertySymbols 和 Reflect.ownKeys。这正是元数据需要的行为:专门去找时能看到,只遍历字符串键的代码则看不到。
用 Symbol.for 共享 Symbol
Symbol() 给你的是一个局部的、一次性的 Symbol。有时你需要的恰恰相反:一个在整个程序、跨模块都相同的 Symbol,让不同部分的代码能约定同一个键。这就是 Symbol.for 的用途。
Symbol.for(key) 会检查一个全局注册表:如果这个键对应的 Symbol 已经存在,就把它返回给你;否则创建一个并存进去。Symbol.keyFor 反过来,返回已注册 Symbol 创建时用的键(对未注册的 Symbol 返回 undefined)。
大多数时候,普通的 Symbol() 就够用了。只有当 Symbol 确实需要跨模块共享时,再用 Symbol.for。
内置 Symbol:接入语言的钩子
Symbol 真正的价值体现在这里。JavaScript 提供了一组预定义的 Symbol,也就是内置 Symbol(well-known symbols),语言本身会在你的对象上检查它们。在其中某个键上实现一个方法,你的对象就能直接接入对应的内置特性。
最有用的是 Symbol.iterator。任何在这个键上有方法的对象都是可迭代的:它可以用于 for...of、展开语法、解构和 Array.from。
range 不是数组,它只是一个普通对象,带一个以特殊键命名的方法。但因为这个方法返回一个迭代器,for...of 和展开语法都能用在它上面,和用在数组、字符串、Map 上一样。约定就是这样:实现 Symbol.iterator,语言就把你当作可迭代对象。
值得了解的其他内置 Symbol
还有几个内置 Symbol,每个都是接入语言不同部分的钩子:
Symbol.iterator:让对象可迭代。Symbol.asyncIterator:作用相同,但用于for await...of。Symbol.toPrimitive:控制对象在类型转换场景下如何转换成原始值(number、string 或 default)。Symbol.hasInstance:自定义instanceof对你的类返回什么。Symbol.toStringTag:设置Object.prototype.toString.call(obj)中显示的标签。
你不需要把它们都背下来,知道有这些东西就够了。当你希望自己的对象表现得像内置类型时,通常都有一个对应的内置 Symbol。
Symbol 不是字符串
一个常见的坑:Symbol 不会自动转换成字符串。把它拼接到字符串上会抛出错误,把它传给需要字符串的 API 也一样:
模板字符串同样会抛出 TypeError。真的需要文本时,用 String(symbol) 或 symbol.toString()。这些限制是有意为之:语言在防止你不小心把一个唯一的身份值当成普通字符串数据来处理。
什么时候用 Symbol(以及什么时候不用)
以下情况适合用 Symbol:
- 你要给不属于你的对象附加元数据,需要一个不会冲突的键。
- 你在设计一种协议:“想和我的库配合的对象,应该在这个 Symbol 上实现一个方法。”
- 你想要一个不会出现在
JSON.stringify或for...in中的属性。 - 你在实现
Symbol.iterator或其他内置 Symbol。
以下情况不必用 Symbol:
- 你只是需要一个对象键,而且没有冲突风险。字符串更简单,在日志里也直接显示它本身。
- 你想要“私有”字段。以 Symbol 为键的属性并不是真正私有的,
Object.getOwnPropertySymbols能找到它们。要真正的私有,请用类的#private字段。 - 你存的数据需要经过
JSON.stringify保留下来。Symbol 属性会丢失。
大多数 JavaScript 代码从来不需要直接写 Symbol(...)。但一旦你希望自己的对象能用于 for...of,或者在类型转换场景中表现自然,Symbol 就是通往这套机制的入口。
下一节:函数声明
Symbol、迭代器和生成器都大量依赖函数:存放在 Symbol.iterator 上的方法,返回迭代器的工厂函数,还有用 function* 形式省掉大部分样板代码的生成器。函数是下一章的内容,我们从声明函数的几种方式以及它们之间的区别讲起。
常见问题
JavaScript 中的 Symbol 是什么?
Symbol 是一种原始类型,它的值保证是唯一的。你可以用 Symbol() 或 Symbol('description') 创建一个 Symbol,任意两次调用得到的 Symbol 都不相等。它主要用作不会和其他键冲突的对象键,以及通过 Symbol.iterator 这类内置 Symbol 接入语言特性。
什么时候应该用 Symbol 而不是字符串作键?
当你想给对象加一个属性,又不想和其他代码的属性重名时,就用 Symbol:比如库要附加元数据,框架要给对象打标记,或者某个键本来就不打算作为公开 API 的一部分。对于日常使用、注重可读性而且不存在冲突风险的键,字符串仍然是正确的选择。
Symbol.iterator 有什么用?
Symbol.iterator 是一个内置 Symbol,它告诉 for...of、展开语法和解构如何遍历一个对象。如果你在 Symbol.iterator 这个键上定义一个返回迭代器的方法,你的对象就变成了可迭代对象。数组、字符串、Map 和 Set 在底层就是这样工作的。
Symbol() 和 Symbol.for() 有什么区别?
Symbol('x') 每次调用都会创建一个全新的 Symbol,即使描述相同,两次调用的结果也不一样。Symbol.for('x') 会查找一个全局注册表,在整个程序里对同一个键返回同一个 Symbol。需要在模块或 realm 之间共享 Symbol 时用 Symbol.for;只需要局部唯一时用 Symbol()。