概述
ES5 的對(duì)象屬性名都是字符串,這容易造成屬性名的沖突。比如,你使用了一個(gè)他人提供的對(duì)象践图,但又想為這個(gè)對(duì)象添加新的方法(mixin 模式),新方法的名字就有可能與現(xiàn)有方法產(chǎn)生沖突沉馆。如果有一種機(jī)制码党,保證每個(gè)屬性的名字都是獨(dú)一無二的就好了,這樣就從根本上防止屬性名的沖突斥黑。這就是 ES6 引入Symbol的原因揖盘。
Symbol 值通過Symbol函數(shù)生成。這就是說锌奴,對(duì)象的屬性名現(xiàn)在可以有兩種類型兽狭,一種是原來就有的字符串,另一種就是新增的 Symbol 類型鹿蜀。凡是屬性名屬于 Symbol 類型箕慧,就都是獨(dú)一無二的,可以保證不會(huì)與其他屬性名產(chǎn)生沖突耻姥。
注意销钝,Symbol函數(shù)前不能使用new命令有咨,否則會(huì)報(bào)錯(cuò)琐簇。這是因?yàn)樯傻?Symbol 是一個(gè)原始類型的值,不是對(duì)象座享。也就是說婉商,由于 Symbol 值不是對(duì)象,所以不能添加屬性渣叛≌芍龋基本上,它是一種類似于字符串的數(shù)據(jù)類型淳衙。
Symbol函數(shù)可以接受一個(gè)字符串作為參數(shù)蘑秽,表示對(duì) Symbol 實(shí)例的描述饺著,主要是為了在控制臺(tái)顯示,或者轉(zhuǎn)為字符串時(shí)肠牲,比較容易區(qū)分幼衰。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
如果 Symbol 的參數(shù)是一個(gè)對(duì)象,就會(huì)調(diào)用該對(duì)象的toString方法缀雳,將其轉(zhuǎn)為字符串渡嚣,然后才生成一個(gè) Symbol 值。
注意肥印,Symbol函數(shù)的參數(shù)只是表示對(duì)當(dāng)前 Symbol 值的描述识椰,因此相同參數(shù)的Symbol函數(shù)的返回值是不相等的。
Symbol 值不能與其他類型的值進(jìn)行運(yùn)算深碱,會(huì)報(bào)錯(cuò)腹鹉。但是,Symbol 值可以顯式轉(zhuǎn)為字符串敷硅。另外种蘸,Symbol 值也可以轉(zhuǎn)為布爾值,但是不能轉(zhuǎn)為數(shù)值竞膳。
作為屬性名的 Symbol
由于每一個(gè) Symbol 值都是不相等的航瞭,這意味著 Symbol 值可以作為標(biāo)識(shí)符,用于對(duì)象的屬性名坦辟,就能保證不會(huì)出現(xiàn)同名的屬性刊侯。這對(duì)于一個(gè)對(duì)象由多個(gè)模塊構(gòu)成的情況非常有用,能防止某一個(gè)鍵被不小心改寫或覆蓋锉走。
注意滨彻,Symbol 值作為對(duì)象屬性名時(shí),不能用點(diǎn)運(yùn)算符挪蹭。
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
上面代碼中亭饵,因?yàn)辄c(diǎn)運(yùn)算符后面總是字符串,所以不會(huì)讀取mySymbol作為標(biāo)識(shí)名所指代的那個(gè)值梁厉,導(dǎo)致a的屬性名實(shí)際上是一個(gè)字符串辜羊,而不是一個(gè) Symbol 值。
Symbol 類型還可以用于定義一組常量词顾,保證這組常量的值都是不相等的八秃。
還有一點(diǎn)需要注意,Symbol 值作為屬性名時(shí)肉盹,該屬性還是公開屬性昔驱,不是私有屬性。
實(shí)例:消除魔術(shù)字符串
魔術(shù)字符串指的是上忍,在代碼之中多次出現(xiàn)骤肛、與代碼形成強(qiáng)耦合的某一個(gè)具體的字符串或者數(shù)值纳本。風(fēng)格良好的代碼,應(yīng)該盡量消除魔術(shù)字符串腋颠,改由含義清晰的變量代替饮醇。
function getArea(shape, options) {
let area = 0;
switch (shape) {
case 'Triangle': // 魔術(shù)字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 }); // 魔術(shù)字符串
上面代碼中,字符串Triangle就是一個(gè)魔術(shù)字符串秕豫。它多次出現(xiàn)朴艰,與代碼形成“強(qiáng)耦合”,不利于將來的修改和維護(hù)混移。
常用的消除魔術(shù)字符串的方法祠墅,就是把它寫成一個(gè)變量。
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
上面代碼中歌径,我們把Triangle寫成shapeType對(duì)象的triangle屬性毁嗦,這樣就消除了強(qiáng)耦合。
如果仔細(xì)分析回铛,可以發(fā)現(xiàn)shapeType.triangle等于哪個(gè)值并不重要狗准,只要確保不會(huì)跟其他shapeType屬性的值沖突即可。因此茵肃,這里就很適合改用 Symbol 值腔长。
const shapeType = {
triangle: Symbol()
};
屬性名的遍歷
Symbol 作為屬性名,該屬性不會(huì)出現(xiàn)在for...in验残、for...of循環(huán)中捞附,也不會(huì)被Object.keys()、Object.getOwnPropertyNames()您没、JSON.stringify()返回鸟召。但是,它也不是私有屬性氨鹏,有一個(gè)Object.getOwnPropertySymbols方法欧募,可以獲取指定對(duì)象的所有 Symbol 屬性名。
另一個(gè)新的 API仆抵,Reflect.ownKeys方法可以返回所有類型的鍵名跟继,包括常規(guī)鍵名和 Symbol 鍵名。
由于以 Symbol 值作為名稱的屬性肢础,不會(huì)被常規(guī)方法遍歷得到还栓。我們可以利用這個(gè)特性,為對(duì)象定義一些非私有的传轰、但又希望只用于內(nèi)部的方法。
let size = Symbol('size');
如一個(gè)數(shù)組的大小谷婆。
Symbol.for()慨蛙,Symbol.keyFor()
有時(shí)辽聊,我們希望重新使用同一個(gè) Symbol 值,Symbol.for方法可以做到這一點(diǎn)期贫。它接受一個(gè)字符串作為參數(shù)跟匆,然后搜索有沒有以該參數(shù)作為名稱的 Symbol 值。如果有通砍,就返回這個(gè) Symbol 值玛臂,否則就新建并返回一個(gè)以該字符串為名稱的 Symbol 值。
比如封孙,如果你調(diào)用Symbol.for("cat")30 次迹冤,每次都會(huì)返回同一個(gè) Symbol 值,但是調(diào)用Symbol("cat")30 次虎忌,會(huì)返回 30 個(gè)不同的 Symbol 值泡徙。
Symbol.keyFor方法返回一個(gè)已登記的 Symbol 類型值的key。
需要注意的是膜蠢,Symbol.for為 Symbol 值登記的名字堪藐,是全局環(huán)境的,可以在不同的 iframe 或 service worker 中取到同一個(gè)值挑围。
實(shí)例:模塊的 Singleton 模式
Singleton 模式指的是調(diào)用一個(gè)類礁竞,任何時(shí)候返回的都是同一個(gè)實(shí)例。
對(duì)于 Node 來說杉辙,模塊文件可以看成是一個(gè)類苏章。怎么保證每次執(zhí)行這個(gè)模塊文件,返回的都是同一個(gè)實(shí)例呢奏瞬?
很容易想到枫绅,可以把實(shí)例放到頂層對(duì)象global。
// mod.js
function A() {
this.foo = 'hello';
}
if (!global._foo) {
global._foo = new A();
}
module.exports = global._foo;
然后硼端,加載上面的mod.js并淋。
const a = require('./mod.js');
console.log(a.foo);
上面代碼中,變量a任何時(shí)候加載的都是A的同一個(gè)實(shí)例珍昨。
但是县耽,這里有一個(gè)問題,全局變量global._foo是可寫的镣典,任何文件都可以修改兔毙。
為了防止這種情況出現(xiàn),我們就可以使用 Symbol兄春。
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];
但由上面的代碼可知澎剥,這個(gè)東西Symbol.for('foo');是全局的,我們?cè)谄渌胤絝or一下也可以得到這個(gè)key值赶舆,還是可以修改哑姚,所以我們可以直接用非for的Symbol方法祭饭,但是這樣也不絕對(duì)安全可靠,因?yàn)槊看螆?zhí)行腳本就會(huì)生成新的key....
內(nèi)置的 Symbol 值
除了定義自己使用的 Symbol 值以外叙量,ES6 還提供了 11 個(gè)內(nèi)置的 Symbol 值倡蝙,指向語言內(nèi)部使用的方法。
- Symbol.hasInstance
其他對(duì)象使用instanceof運(yùn)算符绞佩,判斷是否為該對(duì)象的實(shí)例時(shí)寺鸥,會(huì)調(diào)用這個(gè)方法。比如品山,foo instanceof Foo在語言內(nèi)部胆建,實(shí)際調(diào)用的是FooSymbol.hasInstance。