# ES6 Symbol

# 前言
此文对ES6中涉及的Symbol类型做了简单说明,也包括部分开放的内置Symbol。
# 属性方法
;Symbol (opens new window) 为符号类型,属于基本数据类型之一。
基本数据类型 (opens new window) 也称为原始数据类型,包括
String、Number、Boolean、undefined、null、Symbol、BigInt,其中Symbol和BigInt为ES6新增
;Symbol()可以用来生成唯一值,也是ES6引入Symbol的原因。
Symbol() === Symbol() // false
创建一个Symbol包装对象。
var sym = Symbol()
var object = Object(sym) // Symbol {Symbol(), description: undefined}
typeof sym // symbol
typeof object // object
# Symbol.prototype.description
;Symbol.prototype.description (opens new window) 用于返回Symbol的描述信息,String或toString方法会包含Symbol()字符串。
var sym = Symbol('desc')
sym.description // desc
sym.toString() // Symbol(desc)
String(sym) // Symbol(desc)
# Symbol.for
与Symbol()不同的是,Symbol.for (opens new window) 除了会创建Symbol符号之外,还会把它放入全局的Symbol注册表。
注册表可以想象为一个对象,键
key为Symbol的描述信息,键值为Symbol符号
;Symbol.for()并非每次都会创建一个新的Symbol,而是检查指定key是否已经在注册表中,若在则返回已保存的Symbol,否则就新建一个并放入全局注册表。
Symbol.for('desc') === Symbol.for('desc') // true
# Symbol.keyFor
;Symbol.keyFor (opens new window) 用于获取指定的Symbol符号,存储在全局注册表里对应的key键。
var s = Symbol(),
y = Symbol.for(),
m = Symbol.for('desc')
Symbol.keyFor(s) // undefined
Symbol.keyFor(y) // 'undefined'
Symbol.keyFor(m) // 'desc'
注意
s和y符号,分别返回undefined和字符串'undefined'
可封装工具函数,判断是否位于全局注册表中。
function inGlobalRegistry(sym) {
return !!Symbol.keyFor(sym)
}
# Symbol
;ES6开放了一部分内置的Symbol符号,注意规范中内置符号前缀为@@,例如@@hasInstance表示Symbol.hasInstance。
# Symbol.hasInstance
;instanceof (opens new window) 用于检测构造函数的原型是否在实例对象的原型链上。
function F() { }
var f = new F()
f instanceof F // true
f instanceof Object // true
函数实现。
function instanceOf(object, constructor) {
// or object.__proto__
while ((object = Object.getPrototypeOf(object))) {
if (object === constructor.prototype) {
return true
}
}
return false
}
instanceOf(String, Object) // true
;instanceof在语言内部将执行 Symbol.hasInstance (opens new window),例如f instanceof F即执行的是F[Symbol.hasInstance](f)。
function F() { }
var f = new F()
f instanceof F // true
F[Symbol.hasInstance](f) // true
自定义instanceof。
var F = {
[Symbol.hasInstance](v) {
return v % 2 === 0
},
}
1 instanceof F // false
2 instanceof F // true
# Symbol.isConcatSpreadable
;Symbol.isConcatSpreadable (opens new window) 即数组或类数组在被concat拼接时,控制是否能展开。
数组Symbol.isConcatSpreadable属性默认为undefined,可以展开。
var array = [1, 2]
[0].concat(array, 3) // [0, 1, 2, 3]
array[Symbol.isConcatSpreadable] = false
[0].concat(array, 3) // [0, [1, 2], 3]
而类数组默认不可展开。
var arrayLike = {
0: 1,
1: 2,
length: 2,
}
[0].concat(arrayLike, 3) // [0, { 0: 1, 1: 2, length: 2 }, 3]
arrayLike[Symbol.isConcatSpreadable] = true
[0].concat(arrayLike, 3) // [0, 1, 2, 3]
# Symbol.species
;ES6中extends存在一个有趣的现象,即内置方法返回的对象都将默认成为派生类的实例。
什么意思呢?
例如SortedArray继承自Array,而内置方法map返回的数组成为了SortedArray的实例。
class SortedArray extends Array {}
const sortedArray = new SortedArray(3, 1, 2)
const array = sortedArray.map(e => e)
array instanceof SortedArray // true
如何做到的呢?
以下为Array类内部的大致结构。
class Array {
static get [Symbol.species]() {
return this
}
...
map(callback) {
const Constructor = this.constructor[Symbol.species]()
const result = new Constructor(this.length)
for (var i = 0; index < result.length; i++) {
result[i] = callback(i, this[i], this)
}
return result
}
}
运行sortedArray.map()时,map函数内this为sortedArray实例,this.constructor为SortedArray派生类。另外静态取值方法[Symbol.species]()内部返回调用者,则this.constructor[Symbol.species]()为SortedArray类。
故sortedArray.map()返回的数组也就是由SortedArray类创建的,array instanceof SortedArray也就必然为true了。
;ES6中将Symbol.species开放,子类可以覆盖父类的[Symbol.species]()静态方法。
class SortedArray extends Array {
static get [Symbol.species]() {
return Array
}
}
const sortedArray = new SortedArray(3, 1, 2)
const array = sortedArray.map(e => e)
array instanceof SortedArray // false
array instanceof Array // true
根据刚才的分析,结合Array的内部结构,容易知道sortedArray.map()返回的数组是由Array类创建的,因此array instanceof SortedArray为false。
有何作用呢?
某些类库可能继承至基类,子类使用基类的方法时,更多的,希望返回的对象是基类的实例,而非子类的实例。例如以上SortedArray继承至基类Array,子类实例sortedArray使用map方法时,希望返回的数组是Array的实例。
所以 Symbol.species (opens new window) 作用为,子类继承基类,子类方法返回新对象时,指定新对象的类(或者说构造函数)。
# Symbol.match
;Symbol.match (opens new window) 即String.prototype.math在语言内部将执行RegExp.prototype[Symbol.match]。
var regexp = /llo/, s = 'hello'
s.match(regexp) // ['llo', index: 2, input: 'hello', groups: undefined]
regexp[Symbol.match](s) // ['llo', index: 2, input: 'hello', groups: undefined]
# Symbol.replace
;Symbol.replace (opens new window) 即String.prototype.replace在语言内部将执行RegExp.prototype[Symbol.replace]。
var regexp = /llo/, s = 'hello'
s.replace(regexp, 'he') // hehe
regexp[Symbol.replace](s, 'he') // hehe
# Symbol.search
;Symbol.search (opens new window) 即String.prototype.search在语言内部将执行RegExp.prototype[Symbol.search]。
var regexp = /llo/, s = 'hello'
s.search(regexp) // 2
regexp[Symbol.search](s) // 2
# Symbol.split
;Symbol.split (opens new window) 即String.prototype.split在语言内部将执行RegExp.prototype[Symbol.split]。
var regexp = new RegExp(''), s = 'hello'
s.split(regexp, 3) // ['h', 'e', 'l']
regexp[Symbol.split](s, 3) // ['h', 'e', 'l']
String.prototype.split()方法中第一个参数为字符串或者正则表达式,第二个参数用于限制分割后的数组长度。
# Symbol.iterator
;Symbol.iterator (opens new window) 为对象部署迭代器,可被用于for...of循环、拓展运算符和解构等。
const object = { foo: 1, bar: 2 }
object[Symbol.iterator] = function () {
const keys = Object.keys(this)
var index = 0
return {
next() {
return {
done: index === keys.length,
value: keys[index++],
}
},
}
}
for (const key of object) {
console.log(key)
// foo
// bar
}
[...object] // ['foo', 'bar']
# Symbol.toPrimitive
对象转换为原始值时,在JavaScript内部会进行 ToPrimitive (opens new window) 抽象运算。
例如将对象转换为字符串类型。
String({ foo: 1 }) // [object Object]
;ToPrimitive抽象运算可以想象为一个ToPrimitive(input, preferredType)方法,input为被转换对象,preferredType为期望返回的结果类型。
;preferredType包括number、string和default三种,不同场景下preferredType值不同。
number:+object正运算、Number(object)等string:${object}模板字符串插值、foo[object]对象用作属性、string.search(object)、String(object)、parseInt(object)等default:object + x加法运算、object == x相等判断等
例如
Number(object)运算场景下,preferredType为number
# ToPrimitive
;ToPrimitive(input, preferredType)运算过程简述为。
- 判断
input是否为非对象(原始值),是则返回input - 否则,判断对象是否有
[Symbol.toPrimitive](hint){}方法,若有- 将
hint参数值初始化为preferredType。注意若preferredType不存在,hint默认为default - 若方法的执行结果为非对象(原始值),则返回,否则抛出
TypeError错误
- 将
- 否则,执行
OrdinaryToPrimitive(input, preferredType)
const foo = {
[Symbol.toPrimitive](hint) {
return '1.00'
},
}
const bar = {
[Symbol.toPrimitive](hint) {
return {}
},
}
Number(foo) // 1
Number(bar) // Uncaught TypeError: Cannot convert object to primitive value at Number
# OrdinaryToPrimitive
;OrdinaryToPrimitive(input, hint)运算过程简述为。
- 若
hint为string,先调用toString(),如果为非对象(原始值)那么返回它。否则再调用valueOf(),如果为非对象(原始值)那么返回它,否则抛出TypeError错误 - 若
hint为number/default,恰好相反,会先调用valueOf(),再调用toString()
const foo = {
valueOf() {
return {}
},
toString() {
return '1.00'
},
}
const bar = {
valueOf() {
return {}
},
toString() {
return {}
},
}
Number(foo) // 1
Number(bar) // Uncaught TypeError: Cannot convert object to primitive value at Number
# 用例
可能你会问,抽象方法很少用到吧。
并非哦,我们以计算[1, 2] + {}结果为例。
实际形如 x + y (opens new window) 的表达式,将分别对x和y执行ToPrimitive(input, preferredType)抽象运算,转化为原始值。
参考刚才的场景,容易知道preferredType为default,即调用valueOf()和toString()。
var x = [1, 2], y = {}
x.valueOf() // [1, 2]
x.toString() // '1, 2'
y.valueOf() // {}
y.toString() // '[object Object]'
所以x + y结果为1, 2[object Object]。
# Symbol.toStringTag
;Symbol.toStringTag (opens new window) 用于向Object.prototype.toString提供标签。
var object = {
[Symbol.toStringTag]: 'Hello',
}
object.toString() // [object Hello]
Object.prototype.toString.call(object) // [object Hello]
object.toString()与Object.prototype.toString.call(object)两者是等价的
# Symbol.unscopables
# with
以下代码中,console.log将沿着fn函数作用域、全局作用域依次寻找foo。
var foo = 1
function fn() {
console.log(foo) // 1
}
fn()
函数fn引入with,console.log将沿着object对象、fn函数作用域、全局作用域寻找foo。
var foo = 1
function fn() {
var object = { foo: 2 }
with (object) {
console.log(foo) // 2
}
}
fn()
因此 with (opens new window) 作用非常简单,即扩展了语句的作用域。
注意若对象上没有某个属性,则将会沿着作用域向上寻找对应变量,若都没有则将抛出错误。
var foo = 1, bar = 3
function fn() {
var object = { foo: 2 }
with (object) {
console.log(bar) // 3
console.log(baz) // Uncaught ReferenceError: baz is not defined
}
}
fn()
;with优势即可以使内部表达式更加简洁,但是语义会不明显,且属性的寻找实际上更加耗时,得不偿失。
function fn() {
var object = { foo: 1, bar: 2, baz: 3 }
with (object) {
// 等价于 object.foo + object.bar + object.baz
console.log(foo + bar + baz) // 6
}
}
fn()
缺点也很明显,严重时将造成代码歧义。例如以下y可能是x的属性值,也可能是函数的第二个参数y。
function fn(x, y) {
with (x) {
console.log(y)
}
}
fn({ y: 1 }) // 1
fn({}, 2) // 2
# 兼容性
在框架 extjs (opens new window) 中存在类似如下的代码。
function fn(values) {
with (values) {
console.log(values)
}
}
fn([])
在ES5浏览器中(例如IE10),可能为values.values属性值或者为函数参数values。而values.values属性并不存在,则将沿着作用域寻找到values变量,输出[]。
实际上并没有什么问题,对吧?
但是在ES6中,数组原型上部署了values方法,values.values将会获取为数组原型的values方法,输出values(){ }。
呐,问题就严重咯~
代码在行为上与以前不一致了,规范的兼容性被破坏了。
思考下怎么解决呢?
能不能在with (object) { }上定义一个规则,让内部不会在对象object上寻找属性呢。
也就有了 Symbol.unscopables (opens new window),用于排除with环境中的属性。
var foo = 1
function fn() {
var object = {
foo: 2,
[Symbol.unscopables]: {
foo: true,
},
}
with (object) {
console.log(foo) // 1
}
}
fn()
数组原型上的Symbol.unscopables属性。
Array.prototype[Symbol.unscopables]
// {
// ...
// keys: true,
// values: true,
// }
也就表示数组默认包含Symbol.unscopables属性,因此以下代码在with环境中就排除了values属性。在ES6浏览器中,将输出[],与ES5的结果一致。
function fn(values) {
with (values) {
console.log(values)
}
}
fn([])
所以引入Symbol.unscopables,仅仅是为了解决with执行环境下的历史兼容性问题。
# 严格模式
可能你会问,ES5中严格模式不是已经禁用了with,为何还要在ES6中解决禁用语句的遗留问题?
个人认为目前浏览器还处在支持严格和非严格两种模式的阶段,非严格模式下还是能正常运行with语句,所以始终都存在着潜在的问题。
即由于规范的差异,导致代码的行为不一致。所以终究还是要解决掉,保证向下兼容,即使解决方式不是太友好。
# 参考
- EcmaScript 为什么要引入 @@species (opens new window)
- Symbol.unscopables 存在的历史原因 (opens new window)
- Array.prototype.flatten ? (opens new window)
# 🎉 写在最后
🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!
手动码字,如有错误,欢迎在评论区指正💬~
你的支持就是我更新的最大动力💪~
GitHub (opens new window) / Gitee (opens new window)、GitHub Pages (opens new window)、掘金 (opens new window)、CSDN (opens new window) 同步更新,欢迎关注😉~