前言
記得剛找工作那會,幾種數據類型是必問題,當時的答案一般都是七種——字符串(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 也有它自己的特性:
- 沒有兩個 Symbol 的值是相等的。就像“世上沒有兩片相同的葉子”一樣,任何兩個 Symbol 數據的值都不會相等。
- 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 可用作對象名稱。
~
~
~ 本文完,感謝閲讀!
~
學習有趣的知識,結識有趣的朋友,塑造有趣的靈魂!
我是〖編程三昧〗的作者 隱逸王,我的公眾號是『編程三昧』,歡迎關注,希望大家多多指教!
你來,懷揣期望,我有墨香相迎! 你歸,無論得失,唯以餘韻相贈!
知識與技能並重,內力和外功兼修,理論和實踐兩手都要抓、兩手都要硬!