Symbol:还在用字符串当 key?你真的不怕属性名冲突吗?

67 阅读6分钟

写 JavaScript 的时候,你是不是几乎所有对象的 key 都用字符串?多人协作的时候,会不会总担心“我这里加个 status,会不会把别人写的覆盖了”?
ES6 明明给了我们一个天生独一无二的新类型 —— Symbol,为什么很多人用到现在还几乎没碰过它?

这篇文章就从零开始,带你一步步看清 Symbol 到底解决了什么问题,又该怎么优雅地用好它。

一、JavaScript 的第七种基本类型:Symbol 到底是什么?

先回顾一下 JS 的数据类型:

  • 基本类型(primitive)

    • number
    • string
    • boolean
    • undefined
    • null
    • bigint
    • 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 类型的 key
  • map(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),我们又能有选择地拿回这些属性

既然它既能增强代码健壮性,又能改善多人协作下的可维护性,
你还打算继续“全靠字符串撑场面”吗?