Symbol 不完全模拟实现(ES6)

473 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

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

实现

先一组目标

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 方法控制输出

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 维持一下生活了

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 注册表中!

var symbolTable = {}
_Symbol.for = function(description){
  return symbolTable[description] ? symbolTable[description] : 
  symbolTable[description] = _Symbol(description)
}

image.png

Symbol.keyFor()

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

_Symbol.keyFor = function(symbol){
  for (var key in symbolTable) {
    if(symbolTable[key] === symbol) return key
  }
  return undefined
}

这样就好了吗?

现实很骨感啊

image.png

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

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
}
// 测试用例
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))
  }
})

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

源码地址