最近在學習一些第三方代碼,發現裏面出現了Symbol字段,因爲以前ES6系列梳理有個小暫停,因此本篇開始針對Symbol進行一下學習。
在ES6以前,咱們所知道的JavaScript 數據類型有:es6
在咱們常規的開發過程當中,特別是在多人協做過程當中,老是不可避免的會出現互相沖突覆蓋的狀況。好比,你使用了一個他人提供的對象,但又想爲這個對象添加新的方法(mixin 模式),新方法的名字就有可能與現有方法產生衝突。此時咱們可能會想,若是能有一個不被覆蓋,本身獨一無二的屬性字段該多好啊。從而ES6中引入了Symbol這一個數據類型,來解決這種衝突問題。跨域
Symbol()函數會返回symbol類型的值,該類型具備靜態屬性和靜態方法。它的靜態屬性會暴露幾個內建的成員對象;它的靜態方法會暴露全局的symbol註冊,且相似於內建對象類,但做爲構造函數來講它並不完整,由於它不支持語法:"new Symbol()"。數組
每一個從Symbol()返回的symbol值都是惟一的。一個symbol值能做爲對象屬性的標識符;這是該數據類型僅有的目的。app
直接使用Symbol()建立新的symbol類型,並用一個字符串(可省略)做爲其描述。函數
let sym1 = Symbol(); let sym2 = Symbol('foo'); let sym3 = Symbol('foo');
上面的代碼建立了三個新的symbol類型。Symbol函數能夠接受一個字符串做爲參數,表示對 Symbol 實例的描述,主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。注意,Symbol("foo") 不會強制字符串 「foo」 成爲一個 symbol類型。它每次都會建立一個新的 symbol類型:學習
Symbol("foo") === Symbol("foo"); // false
當使用new運算符的語法時,會拋出TypeError錯誤,這是由於生成的 Symbol 是一個原始類型的值,不是對象。也就是說,因爲 Symbol 值不是對象,因此不能添加屬性。基本上,它是一種相似於字符串的數據類型。spa
let sym = new Symbol(); // TypeError
若是 Symbol 的參數是一個對象,就會調用該對象的toString方法,將其轉爲字符串,而後才生成一個 Symbol 值。debug
const obj = { toString() { return 'abc'; } }; const sym = Symbol(obj); sym // Symbol(abc)
注意,Symbol函數的參數只是表示對當前 Symbol 值的描述,所以相同參數的Symbol函數的返回值是不相等的。
// 沒有參數的狀況 let s1 = Symbol(); let s2 = Symbol(); s1 === s2 // false // 有參數的狀況 let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 === s2 // false
Symbol 值不能與其餘類型的值進行運算,會報錯。設計
let sym = Symbol('My symbol'); "your symbol is " + sym // TypeError: can't convert symbol to string `your symbol is ${sym}` // TypeError: can't convert symbol to string
可是,Symbol值能夠顯式轉爲字符串,或是布爾值,可是不能是數值。code
let sym = Symbol('My symbol'); String(sym) // 'Symbol(My symbol)' sym.toString() // 'Symbol(My symbol)' Boolean(sym) // true !sym // false if (sym) { // ... } Number(sym) // TypeError sym + 2 // TypeError
在前面咱們也說到了,Symbol的出現就是解決覆蓋衝突問題而產生的,它的主要做用也是被用做對象的屬性名來使用,由於它是獨一無二的,這樣就不會出現同名的屬性,也就不會出現覆蓋的狀況了。這對於一個對象由多個模塊構成的狀況很是有用,能防止某一個鍵被不當心改寫或覆蓋。
let mySymbol = Symbol(); // 第一種寫法 let a = {}; a[mySymbol] = 'Hello!'; // 第二種寫法 let a = { [mySymbol]: 'Hello!' }; // 第三種寫法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上寫法都獲得一樣結果 a[mySymbol] // "Hello!"
注意,Symbol 值做爲對象屬性名時,不能用點運算符。
const mySymbol = Symbol(); const a = {}; a.mySymbol = 'Hello!'; a[mySymbol] // undefined a['mySymbol'] // "Hello!"
上面代碼中,由於點運算符後面老是字符串,因此不會讀取mySymbol做爲標識名所指代的那個值,致使a的屬性名其實是一個字符串,而不是一個 Symbol 值。同理,在對象的內部,使用 Symbol 值定義屬性時,Symbol 值必須放在方括號之中。
let s = Symbol(); let obj = { [s]: function (arg) { ... } }; obj[s](123);
Symbol 類型還能夠用於定義一組常量,保證這組常量的值都是不相等的。
const log = {}; log.levels = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn') }; console.log(log.levels.DEBUG, 'debug message'); console.log(log.levels.INFO, 'info message'); // 另外一個例子 const COLOR_RED = Symbol(); const COLOR_GREEN = Symbol(); function getComplement(color) { switch (color) { case COLOR_RED: return COLOR_GREEN; case COLOR_GREEN: return COLOR_RED; default: throw new Error('Undefined color'); } }
常量使用 Symbol 值最大的好處,就是其餘任何值都不可能有相同的值了,所以能夠保證上面的switch語句會按設計的方式工做。
魔術字符串指的是,在代碼之中屢次出現、與代碼造成強耦合的某一個具體的字符串或者數值。
function getArea(shape, options) { let area = 0; switch (shape) { case 'Triangle': // 魔術字符串 area = .5 * options.width * options.height; break; /* ... more code ... */ } return area; } getArea('Triangle', { width: 100, height: 100 }); // 魔術字符串
上面這類處理咱們可能會感受到很熟悉,這也是平常開發過程當中很是常見的處理方式。而上面的字符串Triangle
就是一個魔術字符串。
而消除魔術字符串的方法就是能夠經過使用Symbol來把它變成一個常量
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 });
這時咱們能夠發現shapeType.triangle
的具體值咱們並不關心,但它偏偏又能知足咱們的需求。
還有一點須要注意,Symbol 值做爲屬性名時,該屬性仍是公開屬性,不是私有屬性。
Symbol 做爲屬性名,該屬性不會出如今for...in、for...of循環中,也不會被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。可是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,能夠獲取指定對象的全部 Symbol 屬性名。
const obj = {}; let a = Symbol('a'); let b = Symbol('b'); obj[a] = 'Hello'; obj[b] = 'World'; const objectSymbols = Object.getOwnPropertySymbols(obj); objectSymbols // [Symbol(a), Symbol(b)]
Reflect.ownKeys方法能夠返回全部類型的鍵名,包括常規鍵名和 Symbol 鍵名。
let obj = { [Symbol('my_key')]: 1, enum: 2, nonEnum: 3 }; Reflect.ownKeys(obj) // ["enum", "nonEnum", Symbol(my_key)]
在上面使用Symbol()
函數的語法,不會在咱們整個代碼庫中建立出一個可用的全局的Symbol類型的變量,可是有時候咱們但願可以從新使用同一個Symbol值。要建立跨文件可用的symbol,甚至跨域(每一個都有它本身的全局做用域),咱們可使用Symbol.for()
方法和 Symbol.keyFor()
方法從全局的Symbol註冊表中建立和獲取Symbol。
let s1 = Symbol.for('foo'); let s2 = Symbol.for('foo'); s1 === s2 // true
Symbol()和Symbol.for()的區別是:前者會被登記在全局環境中供搜索,後者不會。Symbol.for()不會每次調用就返回一個新的 Symbol 類型的值,而是會先檢查給定的key是否已經存在,若是不存在纔會新建一個值。好比,若是你調用Symbol.for("cat")30 次,每次都會返回同一個 Symbol 值,可是調用Symbol("cat")30 次,會返回 30 個不一樣的 Symbol 值
。
Symbol.for("bar") === Symbol.for("bar") // true Symbol("bar") === Symbol("bar") // false
Symbol.keyFor方法返回一個已登記的 Symbol 類型值的key。
let s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo", 已登記的Symbol值 let s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined, 未登記的Symbol值
Symbol.for爲 Symbol 值登記的名字,是全局環境的,能夠在不一樣的 iframe 或 service worker 中取到同一個值
iframe = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo') // true
本次主要針對ES6中新增的數據類型Symbol進行了梳理,關於內置的Symbol值的相關內容,在後續使用到的過程當中再進行分享,基本的一些使用你們能夠參照ECMAScript 6入門中Symbol中的講解。