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 部分的实现:
-
构造器
-
构造器的属性
-
prototype 的属性
-
实例的属性
详解1:构造器
首先我们应当知道,Symbol 可以作为一个函数使用,有一个可选参数,并且会执行以下 4 步:
-
如果作为构造器,使用 new 调用,应当抛出类型错误。
-
如果为传递参数,将描述文字设置为 undefined。
-
否则将参数转换成字符串作为描述文字。
-
返回一个唯一的 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 种:
-
函数:如 for 和 keyFor
-
对象:prototype
-
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 -
vuex mutation-types
我们知道在 vuex 中管理 mutation 的类型通常需要使用如下的方式:
const SOME_MUTATION = 'SOME_MUTATION'有了 Symbol 我们可以这样写:
const SOME_MUTATION = Symbol()
小结
本文简单的实现了一个 Symbol Polyfill,相信读者可以通过本文对 Symbol 有一个更为深刻的理解。
相关链接
1. 规范
2. MDN 文档
3. Github 已有的实现
4. 本文完整实现