Symbol 不完全模拟实现(ES6)

symbolES6 中新增的基本类型,通过 Symbol() 函数返回 symbol 类型的值,值是唯一的。Symbol类似于内建对象类,无法作为构造器使用!

实现

先一组目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const types = [1, '1', false, true, undefined, null, NaN, {}, [], function(){}]
const createSymbols = (fn, test) => {
return test.map(item => fn(item)).reduce((prev, item) => {
prev[item] = String(item)
return prev
}, {})
}
const tests = [...types, ...types]

console.log(createSymbols(Symbol, tests))
/*
{
[Symbol(1)]: 'Symbol(1)',
[Symbol(1)]: 'Symbol(1)',
[Symbol(false)]: 'Symbol(false)',
[Symbol(true)]: 'Symbol(true)',
[Symbol()]: 'Symbol()',
[Symbol(null)]: 'Symbol(null)',
[Symbol(NaN)]: 'Symbol(NaN)',
[Symbol([object Object])]: 'Symbol([object Object])',
[Symbol()]: 'Symbol()',
[Symbol(function(){})]: 'Symbol(function(){})',
[Symbol(1)]: 'Symbol(1)',
[Symbol(1)]: 'Symbol(1)',
[Symbol(false)]: 'Symbol(false)',
[Symbol(true)]: 'Symbol(true)',
[Symbol()]: 'Symbol()',
[Symbol(null)]: 'Symbol(null)',
[Symbol(NaN)]: 'Symbol(NaN)',
[Symbol([object Object])]: 'Symbol([object Object])',
[Symbol()]: 'Symbol()',
[Symbol(function(){})]: 'Symbol(function(){})'
}
*/

唯一值可以通过引用类型实现,通过 toString 方法控制输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var MySymbol = (function(){
// 只要引用不同就不会比较相等
function _Symbol(description){
// 不能被new调用
if(this instanceof _Symbol) throw new TypeError('Symbol is not a constructor')
var desc = description === undefined ? '' : String(description)
var symbol = Object.create({
toString: function(){
return 'Symbol (' + this._description + ')'
}
})
Object.defineProperties(symbol, {
'_description': {
value: desc,
writable: false,
enumerable: false,
configurable: false
}
})
return symbol
}
return _Symbol
})();

理想很丰富,现实很骨感,确实没法用字符串去规避掉作为键值的重复

image.png

只能加个 id 维持一下生活了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var MySymbol = (function(){
var generateString = (function(){
var id = 0
return function(desc){
return 'Symbol(' + (id++) + ') (' + desc + ')'
}
})();
function generateId(val){
return 'Symbol (' + this._description + ')'
}
// 只要引用不同就不会比较相等
function _Symbol(description){
// 不能被new调用
if(this instanceof _Symbol) throw new TypeError('Symbol is not a constructor')
var desc = description === undefined ? '' : String(description)
var symbol = Object.create({
toString: function(){
return this._name
}
})
Object.defineProperties(symbol, {
'_description': {
value: desc,
writable: false,
enumerable: false,
configurable: false
},
'_name': {
value: generateString(desc),
writable: false,
enumerable: false,
configurable: false
}
})
return symbol
}
return _Symbol
})();

抛开 id 不谈,假装我们已经实现了(doge)

image.png

接着得看下 forkeyFor 该如何实现

Symbol.for()

根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中

Symbol() 不同的是,用 Symbol.for() 方法创建的的 symbol 会被放入一个全局 symbol 注册表中!

1
2
3
4
5
var symbolTable = {}
_Symbol.for = function(description){
return symbolTable[description] ? symbolTable[description] :
symbolTable[description] = _Symbol(description)
}

image.png

Symbol.keyFor()

获取全局 symbol 注册表中与某个 symbol 关联的键,通过 Symbol.For 注册的值哦!

1
2
3
4
5
6
_Symbol.keyFor = function(symbol){
for (var key in symbolTable) {
if(symbolTable[key] === symbol) return key
}
return undefined
}

这样就好了吗?

现实很骨感啊

image.png

因为对象键值存储会进行 String() 的类型转换,为了尽可能接近,我们只好用双数组的方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var _symbolKey = []
var _symbolValue = []
_Symbol.for = function(description){
var i = 0
var len = _symbolKey.length
while (i < len) {
if(_symbolKey[i] === description) return _symbolValue[i]
i++
}
_symbolKey.push(description)
_symbolValue.push(_Symbol(description))
return _symbolValue[i]
}
_Symbol.keyFor = function(symbol){
var i = 0
var len = _symbolValue.length
while (i < len) {
if(symbol === _symbolValue[i]) return _symbolKey[i]
i++
}
return undefined
}
1
2
3
4
5
6
7
8
9
// 测试用例
types.map(item => ({ item, symbol: MySymbol.for(item) })).forEach(({ item, symbol }) => {
let sym = MySymbol.keyFor(symbol)
if(!(sym === item)){
if(sym !== sym) return
console.log('no pass')
console.log(item, sym, String(symbol))
}
})

搞定,完成(并不完全模拟)

源码地址