先记住这个答案
Symbol 是 ES6 新增的原始类型,typeof 返回 symbol。它最大的特点是唯一:每次调用 Symbol() 都会生成一个和其它任何值都不相等的新值,括号里的描述只是调试时显示的标签,不参与比较,所以 Symbol('id') !== Symbol('id')。它不能用 new 调用,因为规范要求 Symbol 被 new 时直接抛 TypeError,目的是不让人造出 new Number() 那种容易出错的包装对象。需要在不同地方拿到同一个 Symbol 时,用 Symbol.for(key) 从全局注册表里取。
typeof结果是symbol,是原始值不是对象- 每次
Symbol()都是新值,描述相同也不相等 new Symbol()会抛TypeErrorSymbol.for可从注册表复用值
一段代码看完 Symbol 的特性
唯一性、不能 new、全局注册表、不能隐式转字符串,这四点放在一起跑一遍最直观。
const a = Symbol('id')
const b = Symbol('id')
// 描述只是标签,不参与比较
console.log(typeof a, a === b, a.description)
// 不能 new:规范直接禁止
try {
new Symbol('id')
} catch (err) {
console.log(err.name)
}
// 全局注册表:同一个 key 拿到同一个 Symbol
console.log(Symbol.for('id') === Symbol.for('id'), Symbol.keyFor(Symbol.for('id')))
// 不能隐式转字符串,但可以显式转
try {
console.log('key: ' + a)
} catch (err) {
console.log(err.name)
}
console.log(String(a), a.toString())查看输出与解释
symbol false id
TypeError
true id
TypeError
Symbol(id) Symbol(id)Symbol.keyFor 是 Symbol.for 的反向操作,传入注册表里的 Symbol 返回它的 key。对普通 Symbol() 创建的值调用它会返回 undefined,因为那些值从来没进过注册表。
'key: ' + a 抛错是故意的设计。如果 Symbol 能悄悄变成字符串 'Symbol(id)',拿它当属性名拼接时就会退化成普通字符串键,唯一性也就没了。所以规范只允许显式转换。
为什么要禁止 new
new Number(1)、new String('a') 这类写法会得到一个对象,typeof 是 object,放进 if 里永远为真,new Boolean(false) 也是真值,这是老 JavaScript 里公认的坑。Symbol 是 ES6 才加入的类型,设计时就直接堵掉了这条路:用 new 调用 Symbol 抛 TypeError。
包装对象本身还存在,只是不能用 new 得到。写 sym.description 时引擎会临时装箱,调用完就丢掉;真需要一个 Symbol 包装对象可以写 Object(sym),但实际开发里几乎用不到。后来新增的 BigInt 也沿用了同样的规则,new BigInt(1) 同样抛 TypeError。
容易答错的地方
- 描述相同的 Symbol 是同一个值
- 描述只是标签。
Symbol('id')调用两次得到两个不同的值。只有Symbol.for('id')才会按 key 复用。 - Symbol 不能转成字符串
- 只是不能隐式转换。
String(sym)、sym.toString()都能得到'Symbol(id)',sym.description能拿到描述本身。会抛错的是+拼接和模板字符串插值。 Symbol.for创建的值是局部的- 正好相反,
Symbol.for的注册表是全局的,甚至不同 iframe 里的代码用同一个 key 都会拿到同一个 Symbol。所以它适合跨模块约定一个公共键,但也就不再「别人猜不到」了。
面试官还会怎么问?
Symbol 在实际项目里用来做什么?
最常见的是当对象属性键,避免和别人的字符串属性撞名。另一类是内置的 well-known Symbol,比如实现 Symbol.iterator 让对象支持 for...of,实现 Symbol.toPrimitive 控制类型转换。
Symbol 能当 WeakMap 的键吗?
ES2023 起可以用非注册的 Symbol 当 WeakMap 的键,因为它们和对象一样唯一且可以被回收。Symbol.for 创建的 Symbol 不行,它们存在全局注册表里永远不会被回收,传进去会抛 TypeError。
JSON.stringify 遇到 Symbol 会怎样?
对象里值为 Symbol 的属性会被跳过,Symbol 键也会被跳过;数组里的 Symbol 元素会变成 null;直接 JSON.stringify(Symbol()) 返回 undefined。要保留就得自己在 replacer 里处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。