Symbol Polyfill 填坑之旅

javascript/jquery

浏览数:96

2019-3-6

代码:
https://gist.github.com/liril-net/4436fb0bdc8f8ddecbdd34bdfa571b14

Symbol 是 ECMAScript 2015 中新提出的一个『原始数据类型』,为了增加对其的理解,笔者尝试去实现其 Polyfill ,并在这个过程中,发现了许多有趣的知识点,特此记录。

Symbol 是什么?

要想实现 Symbol 我们要知道 Symbol 是什么,简单来说 『Symbol 是颜色不一样的烟火』。它有以下几个特点:

  • Symbol() 是函数,有一个可选的字符串参数
Symbol()
Symbol('Eleme')
  • 返回的是原始数据类型 Symbol
const ELEME = Symbol('Eleme')
typeof ELEME  // symbol
console.log(ELEME)  // Symbol(Eleme)
  • 具有唯一性
Symbol('Eleme') === Symbol('Eleme')  // false
  • 不允许作为构造器使用,即不允许使用 new
new Symbol('Eleme')  // TypeError: Symbol is not a constructor
  • 不允许隐式转换成字符串
'' + Symbol('Eleme')  // TypeError: Cannot convert a Symbol value to a string
String(Symbol('Eleme'))  // Symbol(Eleme)
  • 不允许转换成数字
1 + Symbol('Eleme')  // TypeError: Cannot convert a Symbol value to a number
Number(Symbol('Eleme'))  // TypeError: Cannot convert a Symbol value to a number
  • 有两个静态方法 for 和 keyFor
typeof Symbol.for  // function
typeof Symbol.keyFor // function
  • 有几个内置 Symbol 属性

接下来我们会根据规范,尝试去实现一个 Polyfill,一共分为 4 部分的实现:

  1. 构造器
  2. 构造器的属性
  3. prototype 的属性
  4. 实例的属性

详解1:构造器

首先我们应当知道,Symbol 可以作为一个函数使用,有一个可选参数,并且会执行以下 4 步:

  1. 如果作为构造器,使用 new 调用,应当抛出类型错误。
  2. 如果为传递参数,将描述文字设置为 undefined。
  3. 否则将参数转换成字符串作为描述文字。
  4. 返回一个唯一的 Symbol 值,它的 Description 设置为上面的描述文字。

值得注意的是 __Description__ 和 __Name__ 实际上都不应当被外界访问到,应作为私有属性。此外,为了使 constructor 的 name 显示 Symbol 我们将简单的进行一下处理。根据这些我们可以实现以下代码,:

const SymbolPolyfill = function Symbol (description) {
  if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor')

  let descString
  description === undefined ? descString = undefined : descString = String(description)

  let symbol = Object.create(SymbolPolyfill.prototype)

  defineProperties(symbol, {
    __Description__: d('c', descString),
    __Name__: d('c', generateName(descString)),
  })

  return symbol
}

其中有一个 generateName 函数,用来为每一个 Symbol 生成唯一的名字。

它的一个简单实现版本如下:

const generateName = (function () {
  const created = {}

  return function (description, internal) {
    const postfix = created[description] =  created[description] === undefined ?  1 : created[description] + 1
    return `@@${ description || '' }${ postfix }`
  }
}())

这里用到了一个简单的闭包,来避免外界访问 created 这一对象。created 是用来记录已生成的 symbol 的。

详解2:构造器的属性

接下来,我们要设置构造器的属性。为了方便设置每个属性的 descriptor,我们先封装一个 d 函数,它可以方便我们进行快速的设置属性的 descriptor。

构造器的属性分为 3 种:

  1. 函数:如 for 和 keyFor
  2. 对象:prototype
  3. symbol:其他

其中,for 是在全局注册了一个 symbol,keyFor 是取得这个 symbol 的 key,它们的实现较为简单,值得注意的是,规范中是用 List 来实现的,但是我们这里可以直接利用 JavaScript 的遍历性,使用 Object 作为一个映射,从而方便的进行查找等操作:

const GlobalSymbolRegistry = {}

defineProperties(SymbolPolyfill, {
  for: d('cw', function (key) {
    key = String(key)
    return GlobalSymbolRegistry[key] ? GlobalSymbolRegistry[key] : GlobalSymbolRegistry[key] = SymbolPolyfill(key)
  }),
  keyFor: d('cw', function (symbol) {
    for (let key in globalSymbols) {
      if (globalSymbols[key] === symbol) return key
    }
  }),
  prototype: d('', SymbolPolyfill.prototype),
})

其他内置的 symbol,可以使用如下方式优雅的添加:

const INTERNAL = {
  hasInstance: true,
  isConcatSpreadable: true,
  iterator: true,
  match: true,
  replace: true,
  search: true,
  species: true,
  split: true,
  toPrimitive: true,
  toStringTag: true,
  unscopables: true,
}

Object
  .keys(INTERNAL)
  .forEach(key => {
    defineProperty(SymbolPolyfill, key, d('', SymbolPolyfill(key)))
  })

这里我们想要内置 symbol 的名字可以直接是 @@xxxx,如 @@match,这时候,我们可以对 generateName 进行进一步的改造:

const generateName = (function () {
  const created = {}

  return function (description, internal) {
    let postfix
    if (INTERNAL[description]) {
      postfix = ''
      INTERNAL[description] = false
    } else {
      postfix = created[description] =  created[description] === undefined ?  1 : created[description] + 1
    }
    return `@@${ description || '' }${ postfix }`
  }
}())

详解3:prototype 的属性

prototype 上的属性共有 5 个我们将一一介绍:

  • constructor

由于我们的构造器本身是个 function,它上面自身就带有 prototype,且 prototype 的 constructor 指向自身,所以在这无需特别设置。

  • toString

本身,toString 应当设置为返回 Symbol(description),但是由于我们无法真正的创造原始数据类型,我们创造的 symbol 本身其实是一个对象,无法作为属性的键使用,因此,我们这里进行特殊处理,返回其 __Name__ 作为唯一标识符。

defineProperties(SymbolPolyfill.prototype, {
  // toString: d('cw', function () { return `Symbol(${ validateSymbol(this).__Description__ || '' })` }),
  toString: d('cw', function () { return validateSymbol(this).__Name__; }),
})

其中,我们用到了 validateSymbol ,它是我们实现用来对 Symbol 进行判断处理的。

function isSymbol (value) {
  if (!value) return false
  if (typeof value !== 'object') return false
  if (!value.constructor) return false
  if (value.constructor.name !== 'Symbol') return false
  if (!value.__SymbolData__ || value.__SymbolData__ !== value) return false
  if (value[value.constructor.toStringTag] !== 'Symbol') return false
  return true
}

function validateSymbol (value) {
  if (!isSymbol(value)) return new TypeError(value + 'is not a symbol')
  return value
}
  • valueOf

valueOf 理论上也应当返回原始数据类型,但是实际上也无法实现,但根据规范就是应当返回 __SymbolData-_ 的值。

defineProperties(SymbolPolyfill.prototype, {
  valueOf: d('cw', function () { return validateSymbol(this).__SymbolData__ }),
})
  • [@@toPrimitive]

这个键名其实表示的是 Symbol.toPrimitive 对应的 @@toPrimitive 这一 symbol,它将返回 __SymbolData-_ 的值。

defineProperty(SymbolPolyfill.prototype, SymbolPolyfill.toPrimitive, d('c', function () { return validateSymbol(this).__SymbolData__} ))
  • [@@toStringTag]

这个键名其实表示的是 Symbol.toStringTag 对应的 @@toStringTag 这一 symbol,它的值是字符串 Symbol。

defineProperty(SymbolPolyfill.prototype, SymbolPolyfill.toStringTag, d('c', 'Symbol'))

详解4:实例属性

Symbol 的实例像是普通的 Object,所以它的属性都是继承自 Symbol.prototype 的,此外还有一个 SymbolData,它对应着 Symbol 所对应的对象,因此我们修改一下构造器:

const SymbolPolyfill= function Symbol (description) {
  if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor')

  let descString
  description === undefined ? descString = undefined : descString = String(description)

  let symbol = Object.create(SymbolPolyfill.prototype)

  defineProperties(symbol, {
    __Description__: d('c', descString),
    __Name__: d('c', generateName(descString)),
    __SymbolData__: d('c', symbol),
  })

  return symbol
}

细节

这里有个要注意的细节,symbol instanceOf Symbol 应当返回 false ,为了实现这一点,我们需要额外一个 HiddenSymbol 。

const HiddenSymbol = function Symbol (description) {
  if (this instanceof HiddenSymbol) throw new TypeError('Symbol is not a constructor')
  return SymbolPolyfill(description)
}

const SymbolPolyfill= function Symbol (description) {
  if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor')

  let descString
  description === undefined ? descString = undefined : descString = String(description)

  let symbol = Object.create(HiddenSymbol.prototype)

  defineProperties(symbol, {
    __Description__: d('c', descString),
    __Name__: d('c', generateName(descString)),
    __SymbolData__: d('c', symbol),
  })

  return symbol
}

但是这样的话,toString 和 prototype.constructor 会出现问题,因此仍然要再次进行处理:

defineProperties(HiddenSymbol.prototype, {
  constructor: d('cw', SymbolPolyfill),
  toString: d('cw', function () { return this.__Name__; }),
})

值得注意的是,这样子的话,SymbolPolyfill 本身的 toString 方法将不会被访问,不如将它设置回规范要求的样子。

defineProperties(SymbolPolyfill.prototype, {
  toString: d('cw', function () { return `Symbol(${ validateSymbol(this).__Description__ || '' })` }),
  valueOf: d('cw', function () { return validateSymbol(this).__SymbolData__ }),
})

最后,补全其他 prototype 上的方法:

defineProperty(HiddenSymbol.prototype, SymbolPolyfill.toStringTag, d('c', SymbolPolyfill.prototype[SymbolPolyfill.toStringTag]));
defineProperty(HiddenSymbol.prototype, SymbolPolyfill.toPrimitive, d('c', SymbolPolyfill.prototype[SymbolPolyfill.toPrimitive]));

无法实现的点

有一些点,受限于语言本身以及笔者水平等原因暂时无法实现:

  • typeof

由于无法创建真正的原始数据类型,也就无法让 typeof 显示 symbol。

  • console.log

笔者原先以为,console.log 将会调用 toString 或者 valueOf 方法,后来查阅规范发现并非如此,因此只好作罢。

  • JSON.stringify

理论上,JSON.stringify 将会过滤掉 Symbol 作为键名的属性,但是我们并没有真正的实现原始数据类型 Symbol,因此它其实是转换成字符串后作为键名的,因此也无法实现。

  • string 隐性转换

理论上,Symbol 被隐形转换成字符串时应当报错,但是,笔者并不知道如何判断隐形转换还是显性转换,因此作罢。

  • number 转换

原因同上。

Symbol 的应用

  • for…of

Symbol 的一个典型应用是用于 for…of ,它将根据要遍历对象中 Symbol.iterator 的实现来进行处理。MDN 给的一个典型例子是:

var iterable = {
  [Symbol.iterator]() {
    return {
      i: 0,
      next() {
        if (this.i < 3) {
          return { value: this.i++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (var value of iterable) {
  console.log(value);
}
// 0
// 1
// 2

小结

本文简单的实现了一个 Symbol Polyfill,相信读者可以通过本文对 Symbol 有一个更为深刻的理解。

相关链接

1. 规范

2. MDN 文档

3. Github 已有的实现

4. 本文完整实现