前言
記得剛找工作那會,幾種資料型別是必問題,當時的答案一般都是七種——字串(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 可用作物件名稱。
~
~
~ 本文完,感謝閱讀!
~
學習有趣的知識,結識有趣的朋友,塑造有趣的靈魂!
我是〖程式設計三昧〗的作者 隱逸王,我的公眾號是『程式設計三昧』,歡迎關注,希望大家多多指教!
你來,懷揣期望,我有墨香相迎! 你歸,無論得失,唯以餘韻相贈!
知識與技能並重,內力和外功兼修,理論和實踐兩手都要抓、兩手都要硬!
本作品採用《CC 協議》,轉載必須註明作者和本文連結