概述
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。