ES6 中的 Symbol 是什麼?

yinyiwang 發表於 2021-06-24
JavaScript ES6

前言

記得剛找工作那會,幾種資料型別是必問題,當時的答案一般都是七種——字串(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 可用作物件名稱。

~

~

~ 本文完,感謝閱讀!

~

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

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

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

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

本作品採用《CC 協議》,轉載必須註明作者和本文連結