JavaScript 中的 Symbol 大揭秘
上個(gè)月有部分前端友友私信小編咨詢 JavaScript 中 Symbol 怎么使用的呢?在哪些應(yīng)用場(chǎng)景下會(huì)用到這個(gè)Symbol?有哪些內(nèi)置屬性?等等。問(wèn)的問(wèn)題五花八門,今天無(wú)意中翻到《JavaScript 權(quán)威指南》這本書集中有幾頁(yè)詳細(xì)解讀——Symbol 。
所以小編決定出一期關(guān)于Symbol 讀后感的文章來(lái)深入解讀它。閑話少說(shuō),直接進(jìn)入主題。
Symbol 前身
ES5的對(duì)象屬性名都是字符串,這很容易造成屬性名的沖突。 比如,你使用了一個(gè)他人提供的對(duì)象,但又想為這個(gè)對(duì)象添加新的方法,新方法的名字就有可能與現(xiàn)有方法產(chǎn)生沖突。如果有一種機(jī)制,保證每個(gè)屬性的名字都是獨(dú)一無(wú)二的就好了,這樣就從根本上防止屬性名的沖突。這就是ES6引入的Symbol的原因。
ES6引入了一種新的原始數(shù)據(jù)類型Symbol,表示獨(dú)一無(wú)二的值。它是JavaScript語(yǔ)言的第七種數(shù)據(jù)類型,前六種是:undefined、null、布爾值(Boolean)、字符串(String)、數(shù)值(Number)、對(duì)象(Object)。
Symbol值通過(guò)Symbol函數(shù)生成。這就是說(shuō),對(duì)象的屬性名現(xiàn)在可以有兩種類型,一種是原來(lái)就有的字符串,另一種就是新增的Symbol類型。凡是屬性名屬于Symbol類型,就都是獨(dú)一無(wú)二的,可以保證不會(huì)與其他屬性名產(chǎn)生沖突。
在JavaScript誕生之初,對(duì)象屬性只能使用字符串作為鍵,這導(dǎo)致了一些問(wèn)題。例如,當(dāng)兩個(gè)不同的對(duì)象試圖使用相同的字符串作為屬性名時(shí),可能會(huì)導(dǎo)致屬性名沖突。此外,JavaScript中沒(méi)有一種簡(jiǎn)單的方法來(lái)實(shí)現(xiàn)私有屬性或方法。
其實(shí)對(duì)于Symbol的追溯早在Lisp語(yǔ)言中就有體現(xiàn):
這里其實(shí)就是創(chuàng)建了一個(gè)名為 echa-symbol 的符號(hào)對(duì)象,并將其賦值給變量e。
另外,ES6引入Symbol其實(shí)離不開(kāi)Ruby的身影,在Ruby中,可以使用冒號(hào)(:)來(lái)創(chuàng)建符號(hào)。冒號(hào)后面跟著符號(hào)的名稱,如:
可以看到其實(shí)Ruby的語(yǔ)法更加簡(jiǎn)潔,定義和使用都是用冒號(hào)區(qū)分:
所以,在這樣的需求背景下,ES6在首批特性中包含了Symbol也不足為奇了。
Symbol 介紹
Symbol是ES6中新增的一種原始數(shù)據(jù)類型, 被劃分到了基本數(shù)據(jù)類型中基本數(shù)據(jù)類型: 字符串、數(shù)值、布爾、undefined、null、Symbol 引用數(shù)據(jù)類型: Object。
例如 number, boolean或者null,它們通常用于避免屬性名稱沖突,或模擬 JavaScript 對(duì)象的私有值。
您可以通過(guò)調(diào)用全局函數(shù)來(lái)創(chuàng)建 Symbol():
Symbol() 函數(shù)接受一個(gè)參數(shù),一個(gè)字符串 description 打印 Symbols 時(shí)會(huì)顯示。
主要特征
Symbols 有兩個(gè)關(guān)鍵特征。
第一個(gè)關(guān)鍵特征是 沒(méi)有兩個(gè) Symbols 永遠(yuǎn)相等 。 即使兩個(gè) Symbols 具有相同的描述,它們也不相等。
第二個(gè)關(guān)鍵特性是 對(duì)象鍵可以是 Symbols。通常對(duì)象鍵只能是 Symbols 或字符串。
由于沒(méi)有兩個(gè) Symbols 永遠(yuǎn)相等,因此除非您有權(quán)訪問(wèn) Symbols,否則您無(wú)法訪問(wèn) Symbols 屬性。 這使得 Symbols 可以方便地創(chuàng)建只能在特定函數(shù)中訪問(wèn)的隱藏值。
Symbols 也被排除在外JSON.stringify()輸出,這使得它們非常適合存儲(chǔ)最終用戶不應(yīng)該看到的純程序數(shù)據(jù)。
Symbol 值最顯著的用途是作為對(duì)象屬性的 key。而 ES6 之前,屬性的 key 只能是字符串(否則也會(huì)發(fā)生自動(dòng)類型轉(zhuǎn)換)。key 為 Symbol 值的屬性無(wú)法使用點(diǎn)號(hào)(.)訪問(wèn),只能使用中括號(hào)([])。類似地,在對(duì)象字面值或類定義中,添加 key 為 Symbol 值的屬性需要借助計(jì)算屬性名的語(yǔ)法,如:
JavaScript 模塊化后,在模塊中創(chuàng)建 Symbol 值并用作對(duì)象屬性 key,模塊作者可以自信地認(rèn)為該屬性不會(huì)覆寫對(duì)象上已有的屬性。類似道理,如果模塊中使用了 Symbol key 的對(duì)象屬性,只要不把 Symbol 值導(dǎo)出,就可以確信其他模塊不會(huì)不經(jīng)意地覆寫該屬性。
對(duì)于應(yīng)用開(kāi)發(fā)者,Symbol 可以有這樣的使用場(chǎng)景:你從某第三方代碼獲得了一個(gè)對(duì)象,你想在該對(duì)象上存儲(chǔ)自己的屬性,但該第三方代碼是不透明的,或者不受你控制,所以你無(wú)從得知第三方代碼會(huì)在該對(duì)象上設(shè)置什么屬性,此時(shí)就可以使用 Symbol key 屬性,既能確信不會(huì)和已有屬性沖突,也能確信第三方代碼不會(huì)意外地修改你設(shè)置的屬性:
以上場(chǎng)景還可表述為給對(duì)象關(guān)聯(lián)一個(gè)值,但存儲(chǔ)關(guān)聯(lián)值的鍵名希望從機(jī)制上防止和其他屬性名沖突。另外也可用 WeakMap 存儲(chǔ)對(duì)象到其關(guān)聯(lián)值的映射,兩種方法的異曲同工之處是:當(dāng)對(duì)象本身消亡時(shí),該映射關(guān)系也隨之消亡。
然而,必須知道的是,Symbol key 屬性并不是一種安全機(jī)制,第三方代碼仍然可以調(diào)用
Object.getOwnPropertySymbols(o) 獲得對(duì)象上的 Symbol key 屬性,改變屬性值,但此時(shí)就是第三方庫(kù)有意為之,而不是不經(jīng)意的行為了,但這應(yīng)該會(huì)在其文檔上有所提及才對(duì)。
Symbol 使用注意事項(xiàng)
- Symbol是基本數(shù)據(jù)類型!!!!不要加new哦
- 后面括號(hào)可以傳入一個(gè)字符串,只是一個(gè)標(biāo)記,方便我們閱讀,沒(méi)有任何意義
- 類型轉(zhuǎn)化的時(shí)候不可轉(zhuǎn)化為數(shù)值
- 不能做任何運(yùn)算
- symbol生成的值作為屬性或者方法的時(shí)候,一定要保存下來(lái),否則后續(xù)無(wú)法使用
- for循環(huán)遍歷對(duì)象的時(shí)候是無(wú)法遍歷出symbol的屬性和方法的 Object.getOwnPropertySymbols()
Symbol 的應(yīng)用場(chǎng)景
- 在企業(yè)開(kāi)發(fā)中如果需要對(duì)一些第三方的插件、框架進(jìn)行自定義的時(shí)候可能會(huì)因?yàn)樘砑恿送膶傩曰蛘叻椒? 將框架中原有的屬性或者方法覆蓋掉為了避免這種情況的發(fā)生, 框架的作者或者我們就可以使用Symbol作為屬性或者方法的名稱。
- 消除魔術(shù)字符串
魔術(shù)字符串:在代碼之中多次出現(xiàn)、與代碼形成強(qiáng)耦合的某一個(gè)具體的字符串或者數(shù)值。風(fēng)格良好的代碼,應(yīng)該盡量消除魔術(shù)字符串,改由含義清晰的變量代替。
- 為對(duì)象定義一些非私有的、但又希望只用于內(nèi)部的方法。
由于以 Symbol 值作為鍵名,不會(huì)被常規(guī)方法遍歷得到。我們可以利用這個(gè)特性,為對(duì)象定義一些非私有的、但又希望只用于內(nèi)部的方法。
注意:symbol并不能實(shí)現(xiàn)真正的私有變量的效果,只是不能通過(guò)常規(guī)的遍歷方法拿到symbol類型的屬性而已
再來(lái)復(fù)習(xí)一下對(duì)象的遍歷方法
- for (let xx in obj) :i代表key
- for (let xx of obj):不是自帶的哈
- Object.keys(obj) :返回包含key的數(shù)組
- Object.values(obj) :返回包含value的數(shù)組
- Object.getOwnPropertyNames() :返回包含key的數(shù)組
上述的所有方法都是遍歷不到symbol類型的(注意,是遍歷時(shí)取不到symbol,并不是說(shuō)我們?cè)L問(wèn)不到對(duì)象的symbol類型)
可以遍歷到symbol的方法:
- Object.getOwnPropertySymbols() :返回對(duì)象中只包含symbol類型key的數(shù)組
- Reflect.ownKeys() :返回對(duì)象中所有類型key的數(shù)組(包含symbol)
Symbol 名下哪些屬性?
在 JavaScript 中,Symbol 是一種非常特殊的數(shù)據(jù)類型,可以用來(lái)表示獨(dú)一無(wú)二的值。每個(gè) Symbol 值都是唯一的,一般可以作為對(duì)象屬性的標(biāo)識(shí)符使用,這些屬性稱為 Symbol 屬性,它們不會(huì)與其他屬性產(chǎn)生命名沖突,所以可以用于實(shí)現(xiàn)一些高級(jí)特性和語(yǔ)言擴(kuò)展。
Symbol 還有一些內(nèi)置屬性,比如 Symbol.iterator、Symbol.toPrimitive 和 Symbol.toStringTag 等,它們?cè)趯?shí)現(xiàn)一些特殊的需求(比如自定義對(duì)象的迭代、類型轉(zhuǎn)換和字符串描述時(shí))非常有用,今天我們就來(lái)一起看一下 Symbol 內(nèi)置屬性的一些妙用,看看你知道幾個(gè)?
Symbol.iterator
Symbol.iterator 可以讓我們定義一個(gè)對(duì)象默認(rèn)的迭代器。
有什么用途呢?正常的對(duì)象一般都是不可迭代的,如果我們直接用 for of 遍歷一個(gè)對(duì)象,會(huì)拋出異常:TypeError: obj is not iterable ,因?yàn)閷?duì)象默認(rèn)是不可迭代的。
我們可以用 for in 來(lái)做遍歷:
但是如果我們開(kāi)發(fā)的是一個(gè)自定義的數(shù)據(jù)結(jié)構(gòu),for in 可能就不那么好使了,比如現(xiàn)在有一個(gè)音樂(lè)播放器對(duì)象,我們需要實(shí)現(xiàn)一個(gè)自定義的播放列表,用于存儲(chǔ)用戶添加的歌曲。這個(gè)列表可以看作一個(gè)集合,其中每個(gè)元素代表一個(gè)歌曲,包含歌曲的名稱、作者、時(shí)長(zhǎng)等屬性。
這時(shí)我們就可以通過(guò)定義 Symbol.iterator 方法,讓這個(gè)列表可以通過(guò) for...of循環(huán)進(jìn)行遍歷:
Symbol.toStringTag
默認(rèn)情況下,我們?cè)谌魏我粋€(gè)自定義的對(duì)象上調(diào)用 toString 方法,返回值都是下面這樣:
Symbol.toStringTag 可以被用來(lái)自定義對(duì)象的 toString 方法的返回值(注意不是重寫 toString 方法),這個(gè)特性在調(diào)試的時(shí)候非常有用,幫助開(kāi)發(fā)者更方便地了解對(duì)象的類型信息:
Symbol.toPrimitive
?Symbol.toPrimitive 可以被用來(lái)自定義對(duì)象類型轉(zhuǎn)換時(shí)的行為。它可以接受一個(gè) hint 參數(shù),用于指示對(duì)象應(yīng)該被轉(zhuǎn)換成什么類型的值,比如 Number、String 或者其他默認(rèn)的值。
舉一個(gè)實(shí)際的應(yīng)用場(chǎng)景:假設(shè)我們正在開(kāi)發(fā)一個(gè)日期處理工具,其中需要實(shí)現(xiàn)一個(gè)自定義的日期時(shí)間對(duì)象。這個(gè)對(duì)象包含日期時(shí)間的年月日時(shí)分秒等信息,這時(shí)候就可以用到 Symbol.toPrimitive 方法,來(lái)幫助我們自定義對(duì)象的類型轉(zhuǎn)換行為:
Symbol.asyncIterator
Symbol.asyncIterator 可以用來(lái)實(shí)現(xiàn)一個(gè)對(duì)象的異步迭代器,它可以用于遍歷異步數(shù)據(jù)流,比如異步生成器函數(shù)、異步可迭代對(duì)象等。這個(gè)特性在我們需要處理異步數(shù)據(jù)流時(shí)非常有用。
舉一個(gè)實(shí)際的應(yīng)用場(chǎng)景:假設(shè)我們正在開(kāi)發(fā)一個(gè)異步數(shù)據(jù)源處理器,其中包含了大量的異步數(shù)據(jù),比如網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)查詢等。這些數(shù)據(jù)需要被逐個(gè)獲取并處理,同時(shí)由于數(shù)據(jù)量非常大,一次性獲取全部數(shù)據(jù)會(huì)導(dǎo)致內(nèi)存占用過(guò)大,因此需要使用異步迭代器來(lái)逐個(gè)獲取數(shù)據(jù)并進(jìn)行處理:
Symbol.hasInstance
Symbol.hasInstance可以用于確定一個(gè)對(duì)象是否是某個(gè)構(gòu)造函數(shù)的實(shí)例,它可以用來(lái)改變 instanceof 的行為:
Symbol.species
Symbol.species 可以用于指定創(chuàng)建派生對(duì)象時(shí)要使用的構(gòu)造函數(shù)。一個(gè)實(shí)際的需求場(chǎng)景可能是我們需要對(duì)一個(gè)類進(jìn)行繼承,并且希望這個(gè)類的某些方法返回一個(gè)新的派生對(duì)象而不是基類的實(shí)例對(duì)象。下面是一個(gè)簡(jiǎn)單的例子:
Symbol.match
Symbol.match 可以用來(lái)確定使用 String.prototype.match 方法時(shí)要搜索的值,它可用于更改 match 類 RegExp 對(duì)象的方法行為,下面是一個(gè)實(shí)際實(shí)用案例:
Symbol.replace
S?ymbol.replace 可以幫我們更靈活的處理 String.prototype.replace 方法,比如我們可以自定義字符串的替換行為。
比如有這樣一個(gè)需求場(chǎng)景:我們有一個(gè)字符串處理庫(kù),我們想自定義它的 replace 方法,讓它可以替換一個(gè)字符串的所有元音字母。這時(shí)候就可以用到 Symbol.replace :
Symbol.split
Symbol.split 可以用來(lái)確定使用 String.prototype.split 方法執(zhí)行時(shí)具體要拆分的值。
一個(gè)實(shí)際需求場(chǎng)景:我們需要從文本中提取出所有的數(shù)字。但是文本中的數(shù)字可能包含在不同的字符和符號(hào)中,例如括號(hào)、分隔符和單位等。使用 Symbol.split 可以自定義分割符,這樣我們就可以根據(jù)自己的需求來(lái)對(duì)文本進(jìn)行分割。
Symbol.unscopables
Symbol.unscopables 通常可以用來(lái)避免在使用 with 語(yǔ)句時(shí)訪問(wèn)對(duì)象中不希望被訪問(wèn)的屬性。下面是一個(gè)示例,其中使用 with 語(yǔ)句訪問(wèn)對(duì)象的某些屬性,但通過(guò)將這些屬性添加到 [Symbol.unscopables] 對(duì)象中,可以防止訪問(wèn):
最后
一臺(tái)電腦,一個(gè)鍵盤,盡情揮灑智慧的人生;幾行數(shù)字,幾個(gè)字母,認(rèn)真編寫生活的美好;
一 個(gè)靈感,一段程序,推動(dòng)科技進(jìn)步,促進(jìn)社會(huì)發(fā)展。