實際上,Symbol 的不少特性都沒法模擬實現……因此先讓咱們回顧下有哪些特性,而後挑點能實現的……固然在看的過程當中,你也能夠思考這個特性是否能實現,若是能夠實現,該如何實現。git
ES6 引入了一種新的原始數據類型 Symbol,表示獨一無二的值。github
1. Symbol 值經過 Symbol 函數生成,使用 typeof,結果爲 "symbol"異步
var s = Symbol(); console.log(typeof s); // "symbol"
2. Symbol 函數前不能使用 new 命令,不然會報錯。這是由於生成的 Symbol 是一個原始類型的值,不是對象。函數
3. instanceof 的結果爲 falsepost
var s = Symbol('foo'); console.log(s instanceof Symbol); // false
4. Symbol 函數能夠接受一個字符串做爲參數,表示對 Symbol 實例的描述,主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。this
var s1 = Symbol('foo'); console.log(s1); // Symbol(foo)
5. 若是 Symbol 的參數是一個對象,就會調用該對象的 toString 方法,將其轉爲字符串,而後才生成一個 Symbol 值。code
const obj = { toString() { return 'abc'; } }; const sym = Symbol(obj); console.log(sym); // Symbol(abc)
6. Symbol 函數的參數只是表示對當前 Symbol 值的描述,相同參數的 Symbol 函數的返回值是不相等的。對象
// 沒有參數的狀況 var s1 = Symbol(); var s2 = Symbol(); console.log(s1 === s2); // false // 有參數的狀況 var s1 = Symbol('foo'); var s2 = Symbol('foo'); console.log(s1 === s2); // false
7. Symbol 值不能與其餘類型的值進行運算,會報錯。ip
var sym = Symbol('My symbol'); console.log("your symbol is " + sym); // TypeError: can't convert symbol to string
8. Symbol 值能夠顯式轉爲字符串。作用域
var sym = Symbol('My symbol'); console.log(String(sym)); // 'Symbol(My symbol)' console.log(sym.toString()); // 'Symbol(My symbol)'
9. Symbol 值能夠做爲標識符,用於對象的屬性名,能夠保證不會出現同名的屬性。
var mySymbol = Symbol(); // 第一種寫法 var a = {}; a[mySymbol] = 'Hello!'; // 第二種寫法 var a = { [mySymbol]: 'Hello!' }; // 第三種寫法 var a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上寫法都獲得一樣結果 console.log(a[mySymbol]); // "Hello!"
10. Symbol 做爲屬性名,該屬性不會出如今 for...in、for...of 循環中,也不會被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。可是,它也不是私有屬性,有一個 Object.getOwnPropertySymbols 方法,能夠獲取指定對象的全部 Symbol 屬性名。
var obj = {}; var a = Symbol('a'); var b = Symbol('b'); obj[a] = 'Hello'; obj[b] = 'World'; var objectSymbols = Object.getOwnPropertySymbols(obj); console.log(objectSymbols); // [Symbol(a), Symbol(b)]
11. 若是咱們但願使用同一個 Symbol 值,可使用 Symbol.for。它接受一個字符串做爲參數,而後搜索有沒有以該參數做爲名稱的 Symbol 值。若是有,就返回這個 Symbol 值,不然就新建並返回一個以該字符串爲名稱的 Symbol 值。
var s1 = Symbol.for('foo'); var s2 = Symbol.for('foo'); console.log(s1 === s2); // true
12. Symbol.keyFor 方法返回一個已登記的 Symbol 類型值的 key。
var s1 = Symbol.for("foo"); console.log(Symbol.keyFor(s1)); // "foo" var s2 = Symbol("foo"); console.log(Symbol.keyFor(s2) ); // undefined
看完以上的特性,你以爲哪些特性是能夠模擬實現的呢?
若是咱們要模擬實現一個 Symbol 的話,基本的思路就是構建一個 Symbol 函數,而後直接返回一個獨一無二的值。
不過在此以前,咱們先看看規範中調用 Symbol 時到底作了哪些工做:
Symbol ( [ description ] )
When Symbol is called with optional argument description, the following steps are taken:
- If NewTarget is not undefined, throw a TypeError exception.
- If description is undefined, var descString be undefined.
- Else, var descString be ToString(description).
- ReturnIfAbrupt(descString).
- Return a new unique Symbol value whose [[Description]] value is descString.
當調用 Symbol 的時候,會採用如下步驟:
考慮到還須要定義一個 [[Description]] 屬性,若是直接返回一個基本類型的值,是沒法作到這一點的,因此咱們最終仍是返回一個對象。
參照着規範,其實咱們已經能夠開始寫起來了:
// 初版 (function() { var root = this; var SymbolPolyfill = function Symbol(description) { // 實現特性第 2 點:Symbol 函數前不能使用 new 命令 if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor'); // 實現特性第 5 點:若是 Symbol 的參數是一個對象,就會調用該對象的 toString 方法,將其轉爲字符串,而後才生成一個 Symbol 值。 var descString = description === undefined ? undefined : String(description) var symbol = Object.create(null) Object.defineProperties(symbol, { '__Description__': { value: descString, writable: false, enumerable: false, configurable: false } }); // 實現特性第 6 點,由於調用該方法,返回的是一個新對象,兩個對象之間,只要引用不一樣,就不會相同 return symbol; } root.SymbolPolyfill = SymbolPolyfill; })();
只是參照着規範,咱們已經實現了特性的第 二、五、6 點。
咱們來看看其餘的特性該如何實現:
1. 使用 typeof,結果爲 "symbol"。
利用 ES5,咱們並不能修改 typeof 操做符的結果,因此這個沒法實現。
3. instanceof 的結果爲 false
由於不是經過 new 的方式實現的,因此 instanceof 的結果天然是 false。
4. Symbol 函數能夠接受一個字符串做爲參數,表示對 Symbol 實例的描述。主要是爲了在控制檯顯示,或者轉爲字符串時,比較容易區分。
當咱們打印一個原生 Symbol 值的時候:
console.log(Symbol('1')); // Symbol(1)
但是咱們模擬實現的時候返回的倒是一個對象,因此這個也是沒法實現的,固然你修改 console.log 這個方法是另講。
8. Symbol 值能夠顯式轉爲字符串。
var sym = Symbol('My symbol'); console.log(String(sym)); // 'Symbol(My symbol)' console.log(sym.toString()); // 'Symbol(My symbol)'
當調用 String 方法的時候,若是該對象有 toString 方法,就會調用該 toString 方法,因此咱們只要給返回的對象添加一個 toString 方法,便可實現這兩個效果。
// 第二版 // 前面面代碼相同 …… var symbol = Object.create({ toString: function() { return 'Symbol(' + this.__Description__ + ')'; }, }); // 後面代碼相同 ……
9. Symbol 值能夠做爲標識符,用於對象的屬性名,能夠保證不會出現同名的屬性。
看着好像沒什麼,這點其實和第 8 點是衝突的,這是由於當咱們模擬的所謂 Symbol 值實際上是一個有着 toString 方法的 對象,當對象做爲對象的屬性名的時候,就會進行隱式類型轉換,仍是會調用咱們添加的 toString 方法,對於 Symbol('foo') 和 Symbol('foo')兩個 Symbol 值,雖然描述同樣,可是由於是兩個對象,因此並不相等,可是看成爲對象的屬性名的時候,都會隱式轉換爲 Symbol(foo)
字符串,這個時候就會形成同名的屬性。舉個例子:
var a = SymbolPolyfill('foo'); var b = SymbolPolyfill('foo'); console.log(a === b); // false var o = {}; o[a] = 'hello'; o[b] = 'hi'; console.log(o); // {Symbol(foo): 'hi'}
爲了防止不會出現同名的屬性,畢竟這是一個很是重要的特性,無可奈何,咱們須要修改 toString 方法,讓它返回一個惟一值,因此第 8 點就沒法實現了,並且咱們還須要再寫一個用來生成 惟一值的方法,就命名爲 generateName,咱們將該惟一值添加到返回對象的 __Name__ 屬性中保存下來。
// 第三版 (function() { var root = this; var generateName = (function(){ var postfix = 0; return function(descString){ postfix++; return '@@' + descString + '_' + postfix } })() var SymbolPolyfill = function Symbol(description) { if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor'); var descString = description === undefined ? undefined : String(description) var symbol = Object.create({ toString: function() { return this.__Name__; } }) Object.defineProperties(symbol, { '__Description__': { value: descString, writable: false, enumerable: false, configurable: false }, '__Name__': { value: generateName(descString), writable: false, enumerable: false, configurable: false } }); return symbol; } root.SymbolPolyfill = SymbolPolyfill; })()
此時再看下這個例子:
var a = SymbolPolyfill('foo'); var b = SymbolPolyfill('foo'); console.log(a === b); // false var o = {}; o[a] = 'hello'; o[b] = 'hi'; console.log(o); // Object { "@@foo_1": "hello", "@@foo_2": "hi" }
咱們再看看接下來的特性。
7.Symbol 值不能與其餘類型的值進行運算,會報錯。
以 +
操做符爲例,當進行隱式類型轉換的時候,會先調用對象的 valueOf 方法,若是沒有返回基本值,就會再調用 toString 方法,因此咱們考慮在 valueOf 方法中進行報錯,好比:
var symbol = Object.create({ valueOf: function() { throw new Error('Cannot convert a Symbol value') } }) console.log('1' + symbol); // 報錯
看着很簡單的解決了這個問題,但是若是咱們是顯式調用 valueOf 方法呢?對於一個原生的 Symbol 值:
var s1 = Symbol('foo') console.log(s1.valueOf()); // Symbol(foo)
是的,對於原生 Symbol,顯式調用 valueOf 方法,會直接返回該 Symbol 值,而咱們又沒法判斷是顯式仍是隱式的調用,因此這個咱們就只能實現一半,要否則實現隱式調用報錯,要否則實現顯式調用返回該值,那……咱們選擇不報錯的那個吧,即後者。
咱們無可奈何的修改 valueOf 函數:
// 第四版 // 前面面代碼相同 …… var symbol = Object.create({ toString: function() { return this.__Name__; }, valueOf: function() { return this; } }); // 後面代碼相同 ……
10. Symbol 做爲屬性名,該屬性不會出如今 for...in、for...of 循環中,也不會被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。可是,它也不是私有屬性,有一個 Object.getOwnPropertySymbols 方法,能夠獲取指定對象的全部 Symbol 屬性名。
嗯,沒法實現。
11. 有時,咱們但願從新使用同一個Symbol值,Symbol.for方法能夠作到這一點。它接受一個字符串做爲參數,而後搜索有沒有以該參數做爲名稱的Symbol值。若是有,就返回這個Symbol值,不然就新建並返回一個以該字符串爲名稱的Symbol值。
這個實現相似於函數記憶,咱們創建一個對象,用來儲存已經建立的 Symbol 值便可。
12. Symbol.keyFor 方法返回一個已登記的 Symbol 類型值的 key。
遍歷 forMap,查找該值對應的鍵值便可。
// 第五版 // 前面代碼相同 …… var SymbolPolyfill = function() { ... } var forMap = {}; Object.defineProperties(SymbolPolyfill, { 'for': { value: function(description) { var descString = description === undefined ? undefined : String(description) return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString); }, writable: true, enumerable: false, configurable: true }, 'keyFor': { value: function(symbol) { for (var key in forMap) { if (forMap[key] === symbol) return key; } }, writable: true, enumerable: false, configurable: true } }); // 後面代碼相同 ……
綜上所述:
沒法實現的特性有:一、四、七、八、10
能夠實現的特性有:二、三、五、六、九、十一、12
最後的實現以下:
(function() { var root = this; var generateName = (function(){ var postfix = 0; return function(descString){ postfix++; return '@@' + descString + '_' + postfix } })() var SymbolPolyfill = function Symbol(description) { if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor'); var descString = description === undefined ? undefined : String(description) var symbol = Object.create({ toString: function() { return this.__Name__; }, valueOf: function() { return this; } }) Object.defineProperties(symbol, { '__Description__': { value: descString, writable: false, enumerable: false, configurable: false }, '__Name__': { value: generateName(descString), writable: false, enumerable: false, configurable: false } }); return symbol; } var forMap = {}; Object.defineProperties(SymbolPolyfill, { 'for': { value: function(description) { var descString = description === undefined ? undefined : String(description) return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString); }, writable: true, enumerable: false, configurable: true }, 'keyFor': { value: function(symbol) { for (var key in forMap) { if (forMap[key] === symbol) return key; } }, writable: true, enumerable: false, configurable: true } }); root.SymbolPolyfill = SymbolPolyfill; })()
ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。