写 JavaScript 的时候,你是不是几乎所有对象的 key 都用字符串?多人协作的时候,会不会总担心“我这里加个 status,会不会把别人写的覆盖了”?
ES6 明明给了我们一个天生独一无二的新类型 —— Symbol,为什么很多人用到现在还几乎没碰过它?
这篇文章就从零开始,带你一步步看清 Symbol 到底解决了什么问题,又该怎么优雅地用好它。
一、JavaScript 的第七种基本类型:Symbol 到底是什么?
先回顾一下 JS 的数据类型:
-
基本类型(primitive)
numberstringbooleanundefinednullbigint- symbol
-
引用类型
object
那么,
symbol 和前面的 string / number 有什么本质不同?核心就一句话:
每一个 Symbol 值都是独一无二的。
哪怕你写出两个“看起来一模一样”的 Symbol:
const s1 = Symbol('标签');
const s2 = Symbol('标签');
console.log(typeof s1); // "symbol"
console.log(s1 === s2); // false
描述('标签')一样,值也会完全不同。
既然它这么“孤傲”,不跟任何人相等,那能干嘛?难道只是拿来比较个 false 玩玩吗?
二、为什么需要“独一无二”的值?字符串不够用吗?
平时我们给对象加属性,大部分是这样写的:
const user = {
name: 'Alice',
email: 'alice@example.com'
};
看起来没问题,但换个场景想一下:多人协作、多人维护同一个对象结构的时候,会发生什么?
- A 同学:“我给
user对象加个status字段。” - B 同学:“我也要加个
status表示审核状态。”
结果呢?
后写的直接覆盖先写的。
难道大家永远都要靠“约定好名字”“团队自律”来防止冲突吗?
如果 key 用的是 Symbol 呢?
const STATUS_LOGIN = Symbol('status');
const STATUS_REVIEW = Symbol('status');
const user = {
name: 'Alice',
[STATUS_LOGIN]: 'online', // 登录状态
[STATUS_REVIEW]: 'pending' // 审核状态
};
描述字符串一样,但实际是两个完全不同的 key。
你觉得它们还会互相覆盖吗?
三、Symbol 作为对象 key:真正“不会撞车”的属性名
关键语法只有一个:计算属性名。
const secretKey = Symbol('secret');
const user = {
name: 'Bob',
email: 'bob@example.com',
[secretKey]: '123123' // 注意这里用的是 [ ]
};
几个要点你一定要搞清楚:
-
secretKey自身是一个 Symbol 值 -
user[secretKey] = '123123'会在对象上挂一个 Symbol 类型的 key -
访问的时候也必须用
[]:console.log(user[secretKey]); // '123123' -
你觉得可以用
user.secret拿到吗?当然不行,它压根不是字符串"secret"。
那这样写有啥好处?
- 任何人如果没有拿到
secretKey这个 Symbol 本身,根本没办法碰到这条属性 - 同一个对象上可以有无数个不同的 Symbol key,它们永远互不冲突
难道你不希望自己写的内部字段永远不会被“误伤”吗?
四、为什么说 Symbol 属性是“半隐藏”的?for...in 为什么遍历不到?
再看一个对象的写法:
const classroom = {
[Symbol('Mark')]: { grade: 50, gender: 'male' },
[Symbol('Olivia')]: { grade: 80, gender: 'female' },
[Symbol('Olive')]: { grade: 85, gender: 'female' }
};
这时如果你:
for (const key in classroom) {
console.log(key);
}
会发生什么?
几乎什么都打印不出来。
是不是特别诡异?对象明明有属性,for...in 却什么都看不到?
原因很简单:
for...in只会枚举字符串 key(以及部分数字 key)Object.keys(classroom)也是同样JSON.stringify(classroom)也会直接忽略 Symbol 属性
也就是说,Symbol 属性默认就是“不可枚举”的。
你不觉得这正好很适合放一些“内部用”的数据吗?
五、那我想要拿到这些 Symbol 属性怎么办?真就永远藏起来?
不可枚举不等于永远拿不到。
要“翻出”这些 Symbol 属性,有一个专门的 API:
const syms = Object.getOwnPropertySymbols(classroom);
console.log(syms);
// [ Symbol('Mark'), Symbol('Olivia'), Symbol('Olive') ]
接下来,你就可以基于这些 Symbol key 去取值了:
const data = syms.map(sym => classroom[sym]);
console.log(data);
// [
// { grade: 50, gender: 'male' },
// { grade: 80, gender: 'female' },
// { grade: 85, gender: 'female' }
// ]
这段代码做了什么?
Object.getOwnPropertySymbols(obj):拿到对象上所有 Symbol 类型的 keymap(sym => classroom[sym]):通过每个 Symbol 当 key,取出对应的值,收集成一个新数组
你会发现:
- 对外部调用者来说,这些属性在普通遍历里是“隐形”的
- 对你自己来说,只要通过专门的 API,就能精确控制要不要暴露这些属性
难道这不就是一个“可以控制开关的隐藏字段系统”吗?
每日提升:Symbol 实战场景,不只是“学过”这么简单
光知道语法有什么用?不落地到场景,Symbol 很容易变成“知道但不用”的知识点。
那它到底能用在哪?
1. 避免对象属性名冲突(多人协作 / 库开发)
当你写一个工具库/组件库,往用户传入的对象上加字段时,用字符串是不是很危险?
// 容易和用户自己的字段冲突
obj._internalId = 123;
换成 Symbol:
const INTERNAL_ID = Symbol('internalId');
function attachInternalId(obj, id) {
obj[INTERNAL_ID] = id;
}
谁能轻易覆盖掉这个字段?谁又能轻易拿到它?
没有 INTERNAL_ID 这个 Symbol 本身,几乎没人能访问。
2. 模拟“私有属性”
在没有 class 私有字段(#name)之前,很多人会用 Symbol 模拟“私有变量”:
const _cache = Symbol('cache');
class Service {
constructor() {
this[_cache] = {};
}
set(key, value) {
this[_cache][key] = value;
}
get(key) {
return this[_cache][key];
}
}
外部想直接 service._cache?
根本访问不到。你不觉得比约定俗成的“下划线开头表示私有”可靠多了吗?_
3. 替代“魔法字符串”的枚举常量
你是不是经常这样写状态判断?
if (type === 'success') { ... }
else if (type === 'error') { ... }
一不小心拼错了、或者别人用了同样的字符串,bug 就来了。
换成 Symbol 呢?
const TYPE_SUCCESS = Symbol('success');
const TYPE_ERROR = Symbol('error');
function handle(type) {
if (type === TYPE_SUCCESS) {
// ...
} else if (type === TYPE_ERROR) {
// ...
}
}
别人即使用了同样的描述字符串 'success' 再创建一个 Symbol,也不可能等于你的 TYPE_SUCCESS。
你不觉得这种“防冲突 + 可读性”组合,比普通字符串强太多了吗?
七、总结:为什么现在不用 Symbol,将来一定会后悔?
回顾一下我们得到的结论:
-
Symbol 是第七种基本数据类型,并不是“高级玩具”
-
每一个 Symbol 值天然唯一,再也不用担心 key 撞车
-
把 Symbol 用作对象 key,可以:
- 避免属性名冲突
- 存放“内部/私有”信息
- 让属性在普通遍历中“隐身”
-
通过
Object.getOwnPropertySymbols(obj),我们又能有选择地拿回这些属性
既然它既能增强代码健壮性,又能改善多人协作下的可维护性,
你还打算继续“全靠字符串撑场面”吗?