# 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) 同步更新,欢迎关注😉~