博客 / 詳情

返回

ES6 中的 Symbol 是什麼?

前言

記得剛找工作那會,幾種數據類型是必問題,當時的答案一般都是七種——字符串(String)、數字(Number)、布爾(Boolean)、數組(Array)、對象(Object)、空(Null)、未定義(Undefined),時至今日,某些網絡教程上還是這樣的分類:

不完整的分類

其實,隨着 ECMAScript 的發展和完善,在 ES6(2015) 和 ES11(2020) 中,又分別增加了 Symbol 和 BigInt 兩種類型,所以,完整的分類應該是下面這樣的:

完整的數據類型

今天,我們就來看看 Symbol 到底是什麼類型,為何要引入這樣一個類型。

背景

我們都應該有個清晰的認識:任何新技術或者新概念的出現,必然是為了解決某一痛點的。

想想吧,我們為了起一個漂亮的、符合語義規則的屬性名而絞盡腦汁時的痛苦,還要承受屬性名可能衝突的折磨,那是一段不堪回首的往事!

而 Symbol 的出現正是為了拯救我們的頭髮,讓它們不至於犧牲在這些瑣碎的小事上,它們每一根都是那麼珍貴,它們的歸宿應該在更具價值的地方!

頭髮證的會掉完

概念

symbol 是一種基本數據類型。Symbol() 函數會返回 symbol 類型的值,該類型具有靜態屬性和靜態方法。它的靜態屬性會暴露幾個內建的成員對象;它的靜態方法會暴露全局的 symbol 註冊,且類似於內建對象類,但作為構造函數來説它並不完整,因為它不支持語法:"new Symbol()"。

語法

直接使用 Symbol() 創建新的 symbol 類型,並用一個可選的字符串作為其描述。

Symbol([description])
  • description (可選) 字符串類型。對symbol的描述,可用於調試但不是訪問symbol本身。請注意,即使傳入兩個相同的字符串,得到的 symbol 也不相等。
const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1);
// expected output: "symbol"

console.log(symbol2 === 42);
// expected output: false

console.log(symbol3.toString());
// expected output: "Symbol(foo)"

console.log(Symbol('foo') === Symbol('foo'));
// expected output: false
上面的代碼創建了三個新的 symbol 類型。 注意,Symbol("foo") 不會強制將字符串 “foo” 轉換成 symbol 類型。它每次都會創建一個新的 symbol 類型。

下面帶有 new 運算符的語法將拋出 TypeError 運算符的語法將拋出錯誤:

var sym = new Symbol(); // TypeError

特性

正如歌詞“每個人都有他的脾氣”所説,Symbol 也有它自己的特性:

  1. 沒有兩個 Symbol 的值是相等的。就像“世上沒有兩片相同的葉子”一樣,任何兩個 Symbol 數據的值都不會相等。
  2. Symbol 數據值可以作為對象屬性名。高手一出手,就知有沒有。這一下子就奠定了 Symbol 的江湖地位。要知道,在之前,對象的屬性名是字符串的專屬權利,就連數字也會被同化為字符串,可現在居然被 Symbol 虎口奪食,字符串大概也只能黯然傷神了吧。

用塗

根據 Symbol 的特性,它有以下通途。

命名衝突

JavaScript 內置了一個 symbol ,那就是 ES6 中的 Symbol.iterator。擁有 Symbol.iterator 函數的對象被稱為 可迭代對象 ,就是説你可以在對象上使用 for/of 循環。

const fibonacci = {
    [Symbol.iterator]: function* () {
        let a = 1;
        let b = 1;
        let temp;

        yield b;

        while (true) {
            temp = a;
            a = a + b;
            b = temp;
            yield b;
        }
    }
};

// Prints every Fibonacci number less than 100
for (const x of fibonacci) {
    if (x >= 100) {
        break;
    }
    console.log(x);
}

為什麼這裏要用 Symbol.iterator 而不是字符串?假設不用 Symbol.iterator ,可迭代對象需要有一個字符串屬性名 'iterator',就像下面這個可迭代對象的類:

class MyClass {
    constructor (obj) {
        Object.assign(this, obj);
    }

    iterator() {
        const keys = Object.keys(this);
        let i = 0;
        return (function* () {
            if (i >= keys.length) {
                return;
            }
            yield keys[i++];
        })();
    }
}

MyClass 的實例是可迭代對象,可以遍歷對象上面的屬性。但是上面的類有個潛在的缺陷,假設有個惡意用户給 MyClass 構造函數傳了一個帶有 iterator 屬性的對象:

const obj = new MyClass({ iterator: 'not a function' });

這樣你在 obj 上使用 for/of 的話,JavaScript 會拋出 TypeError: obj is not iterable 異常。

可以看出,傳入對象的 iterator 函數覆蓋了類的 iterator 屬性。

這有點類似原型污染的安全問題,無腦複製用户數據會對一些特殊屬性,比如 proto 和 constructor 帶來問題。

這裏的核心在於,symbol 讓對象的內部數據和用户數據井水不犯河水。

由於 sysmbol 無法在 JSON 裏表示,因此不用擔心給 Express API 傳入帶有不合適的 Symbol.iterator 屬性的數據。另外,對於那種混合了內置函數和用户數據的對象,你可以用 symbol 來確保用户數據不會跟內置屬性衝突。

私有屬性

由於任何兩個 symbol 都是不相等的,在 JavaScript 裏可以很方便地用來模擬私有屬性。symbol` 不會出現在 Object.keys() 的結果中,因此除非你明確地 export 一個 symbol,或者用 Object.getOwnPropertySymbols() 函數獲取,否則其他代碼無法訪問這個屬性。

function getObj() {
    const symbol = Symbol('test');
    const obj = {};
    obj[symbol] = 'test';
    return obj;
}

const obj = getObj();

Object.keys(obj); // []

// 除非有這個 symbol 的引用,否則無法訪問該屬性
obj[Symbol('test')]; // undefined

// 用 getOwnPropertySymbols() 依然可以拿到 symbol 的引用
const [symbol] = Object.getOwnPropertySymbols(obj);
obj[symbol]; // 'test'

還有一個原因是 symbol 不會出現在 JSON.stringify() 的結果裏,確切地説是JSON.stringify()會忽略symbol屬性名和屬性值:

const symbol = Symbol('test');
const obj = { [symbol]: 'test', test: symbol };

JSON.stringify(obj); // "{}"

總結

symbol 具有以下特性:

  • 每個 symbol 都是獨一無二的。
  • symbol 可用作對象名稱。

~

~

~ 本文完,感謝閲讀!

~

學習有趣的知識,結識有趣的朋友,塑造有趣的靈魂!

我是〖編程三昧〗的作者 隱逸王,我的公眾號是『編程三昧』,歡迎關注,希望大家多多指教!

你來,懷揣期望,我有墨香相迎! 你歸,無論得失,唯以餘韻相贈!

知識與技能並重,內力和外功兼修,理論和實踐兩手都要抓、兩手都要硬!

user avatar suporka 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.