前端進階

-

JS Symbol 是什麼?5 分鐘詳解與實戰應用

this.web

Symbol 介紹 - 文章封面

Symbol 是 ES6 引入的 JS 原始型別,他專門用來建立唯一且不可重複的識別符號(unique identifier)。

Symbol 很適合用在需要不重複鍵名封裝屬性的場景。這篇文章就會從頭帶你理解 Symbol 的語法以及實戰應用。

Symbol 基本用法

用法很簡單,直接呼叫 Symbol() 就好,每次呼叫 Symbol() 都會得到全新且不相等的值,這是它的核心特性。

let sym1 = Symbol();
let sym2 = Symbol();

console.log(sym1 === sym2); // false

可以看到上面程式碼的比較會是 false

除此之外,我們也可以加入對於 Symbol 的描述,用來增加可讀性和除錯,不影響唯一性

let id = Symbol('userId');
console.log(id); // Symbol(userId)

console.log(Symbol('userId') === Symbol('userId')); // false

我們也可以直接使用 symbol.description 來直接獲取描述

let id = Symbol('userId');
console.log(id.description); // userId

在物件中用 Symbol 當屬性鍵

由於每一個 Symbol 值都是不相等的,這代表只要我們用 Symbol 值作為物件的屬性,就能保證不會出現同名的屬性。

這樣可以避免命名衝突,也能防止被意外存取、覆蓋或枚舉到,比如說:

const idKey = Symbol('id');

const user = {
  [idKey]: 123,
  name: 'Alice',
  role: 'Admin',
};

console.log(user.idKey);     // undefined
// 因為 . 後面屬於字符串,所以不會讀取到 idKey 這個 Symbol
console.log(user[idKey]);    // 123

// 不會被枚舉到
console.log(Object.keys(user));          // ['name', 'role']
console.log(JSON.stringify(user));        // {"name":"Alice","role":"Admin"}

當你需要在外部套件或共用物件上「安全地」擴充欄位時,用 Symbol 可避免覆蓋原本屬性。

const uniqueToken = Symbol('token');

let session: any = { id: 1001, user: 'Bob' };
session[uniqueToken] = 'secret-token-xyz';

// 不會出現在 for...in / Object.keys() / JSON.stringify()

全域註冊應用:Symbol.for() 與 Symbol.keyFor()

Symbol.for()Symbol.keyFor() 可以讓我們跨模組地共用同一個 Symbol。

我們可以用 Symbol.for() 來宣告一個全局的 Symbol

console.log(Symbol.for('app') === Symbol.for('app')); // true
console.log(Symbol('app') === Symbol('app')); // false

並使用 Symbol.keyFor() 來反為對應的全局 Symbol

const s = Symbol.for('app');
console.log(Symbol.keyFor(s)); // 'app'

Symbol 與 TypeScript 結合:symbol 與 unique symbol

我們可以使用 symbol 當作 type

let s: symbol = Symbol('symbol');

一般 symbol 的限制

但一般的 symbol type 也有一些限制,比如以下程式碼:

const A = Symbol('A');
const B = Symbol('B');

type Shape = typeof A | typeof B;

這裡的 typeof Atypeof B 都只是 symbol 型別。

在 TypeScript 看來,它們沒有差別,都是某個 symbol。

使用 unique symbol 的好處

所以我們可以使用 unique symbol

const TRIANGLE: unique symbol = Symbol('triangle');
const SQUARE:   unique symbol = Symbol('square');
const CIRCLE:   unique symbol = Symbol('circle');

這樣宣告後,typeof TRIANGLEtypeof SQUAREtypeof CIRCLE 都是獨立的型別

於是就可以用:

type Shape = typeof TRIANGLE | typeof SQUARE | typeof CIRCLE;

這樣 TS 編譯器知道:

  • TRIANGLESQUARECIRCLE 各自是不同型別。
  • Shape 是一個 union,可以在 switchif 自動偵測到他的 type。類似 enum 的用法。
function draw(shape: Shape) {
  if (shape === TRIANGLE || shape === SQUARE || shape === CIRCLE) {
		//...
		}
}

實用 Symbol 應用:作為私有鍵(較不易被誤用的欄位)

當你寫第三方套件、要在使用者的物件上加一些內部資訊時,為了避免覆蓋到人家的 props 或 key,就可以使用 Symbol() 產生鍵,因為值是獨一無二的,即便別人也用了同樣的字面名稱,兩個符號仍然不同

而且符號屬性預設不會被列舉,等於把內部欄位「藏」起來,不會和使用者的資料互相干擾。這樣就能安全地在物件上維護內部狀態或標記等等

const INTERNAL_META = Symbol('myLib.internalMeta');

function attachMeta(target, info) {
	target[INTERNAL_META] = {
		info,
		timestamp: Date.now(),
	};
}

function readMeta(target) {
	const meta = target[INTERNAL_META];
	return meta && typeof meta === 'object' ? meta.info : null;
}

// 使用者物件
const userProps = { name: 'Alice', role: 'admin' };
attachMeta(userProps, 'My Info');

console.log(Object.keys(userProps)); // ['name', 'role'] → 不會看到符號欄位
console.log(readMeta(userProps)); // 'My Info'

userProps.INTERNAL_META = 'abc'; // 不會影響到 Symbol
console.log(userProps.INTERNAL_META); // 'abc'
console.log(readMeta(userProps)); // 'My Info'

總結

這篇文章介紹了 JavaScript 在 ES6 中新增的原始型別 —— Symbol,我也說明了它如何在程式中解決屬性衝突、隱藏內部資料,以及與 TypeScript 型別的結合。

Symbol 的核心特點是「唯一且不可重複」

每次呼叫 Symbol() 都會回傳一個全新的值,即使描述文字相同也不會相等。這個特性使它非常適合用作物件的屬性鍵,因為:

  • 不會與其他屬性名稱衝突;
  • 不會被 Object.keys()for...inJSON.stringify() 列舉出來;
  • 能在共用或外部擴充的物件上安全地存放資料。

你可能會感興趣的文章 👇