基础篇
一、什么是Symbol?
Symbol是ES6规范中新引入的一种数据类型,表示独一无二的值。它是JavaScript的第七种数据类型,前六种分别是:Undefined、Null、Boolean、String、Number、Object。
二、为什么要引入Symbol类型?
所有新技术的诞生都是为了解决某一问题或提高生产效率,Symbol也不例外。在ES5时代,对象属性名都是字符串,这就可能出现属性名冲突的情况。例如,当我们引入了一个外部对象,如果我们想在这个对象中添加一个新的方法,就有可能会与对象中现有的属性产生冲突。
想要从根本上解决这个问题,我们需要一个永远不会重复的数据类型,所以Symbol就应运而生了。
三、怎样生成一个Symbol值?
Symbol值可以直接调用Symbol函数生成(还有另外一种生成方法,后面会讲到),每一个Symbol值都是独一无二的,使用typeof检测实例的类型为symbol。
let a = Symbol()
let b = Symbol()
a === b // false
typeof a // symbol
Symbol函数也可以接收一个字符串参数,作为对生成的Symbol描述,其主要目的是为在控制台显示时容易区分不同的Symbol值。请注意,不同的Symbol值可以使用相同的描述,但是不代表他们是相等的。
let a = Symbol()
let b = Symbol('symbol-describe')
let c = Symbol('symbol-describe')
a // Symbol() -->如果不加描述,那么再控制台中输出的都是Symbol()
b // Symbol(symbol-describe)
c // Symbol(symbol-describe)
b === c // false
Symbol值可以显式转为字符串,但不能隐式转换成字符串,也不能和其他任何类型的值进行运算,否则代码会报错!此外,Symbol可以转换为布尔值,但不能转为数值。
let sym = Symbol('symbol')
String(sym) // "Symbol(symbol)"
sym.toString() // "Symbol(symbol)"
'this is' + sym // Cannot convert a Symbol value to a string
`this is ${sym}` // Cannot convert a Symbol value to a string
Boolean(sym) // true
!sym // false
if(sym){
// true
}
Number(sym) // TypeError
需要注意的是:
不能使用new命令来调用Symbol函数,否则代码会报错!因为生成的Symbol值是一个原始类型的值,而不是对象。此外,因为Symbol值是原始类型,所以用instanceof判断Symbol值会直接返回false。
四、Symbol.for( )、Symbol.keyFor( )
除了上述方法外,Symbol.for()方法也可以生成一个Symbol值。Symbol.for()接收一个key值(字符串)作为参数。在运行时,会先在全局环境下搜索,是否有用该key值创建过Symbol实例。如果存在,则不会再新创建一个Symbol值,而是直接返回这个Symbol值。如果不存在,才会创建一个新的Symbol值,并在全局环境中登记,供下次搜索。
let a = Symbol.for('my-symbol')
let b = Symbol.for('my-symbol')
a === b // true --> 因为已经存在以'my-symbol'为key值创建的实例,所以在创建b时直接返回了a
let c = Symbol('symbol')
let d = Symbol.for('symbol')
c === d // false --> Symbol('symbol')方法并不会登记,所以Symbol.for会新创建一个值
与Symbol.for方法对应的还有一个Symbol.keyFor方法,此方法可以获取一个已登记Symbol值的key值。如果Symbol值没有登记,则返回undefined。
let sym1 = Symbol()
let sym2 = Symbol.for('mySymbol')
Symbol.keyFor(sym1) // undefined
Symbol.keyFor(sym2) // "mySymbol"
五、Symbol使用场景
(一)、用作对象属性名
想象一下这个场景,有一天你引入了一个外部提供的对象,你想要在这个对象里加入新的方法,但是你却不知道你定义的属性名已经存在于现有对象中了,这造成了一个让你抓狂bug。。。此时,如果你使用了Symbol作为属性名,就完全不用担心属性名会冲突,因为每一个Symbol都是独一无二的。在对象中使用Symbol属性名有以下三种写法:
let sym = Symbol()
// 写法一
let a = {}
a[sym] = 'juejin'
// 写法二
let a = {
"sym" : 'juejin',
[sym] : 'juejin', // 你可以随意使用自己想用的名字,不会产生冲突
[sym2](){...} // 也可以采用简洁的写法
}
// 写法三
Object.defineProperty(a, sym, {value:'juejin'})
需要注意的是:
1. 不能使用点运算符去定义Symbol类型的属性名,因为点运算符始会把后面的值读取为字符串。
let sym = Symbol()
let obj = {}
obj.sym = 'juejin'
obj[sym] // undefined
obj['sym'] //'juejin'
2. 用for...in、for...of、Object.keys()、Object.getOwnPropertyNames()方法遍历对象时,均不会遍历出Symbol属性名。有两个方法可以获取Symbol类型的属性名:
Object.getOwnPropertySymbols,会返回一个数组,包含当前对象所有用作属性名的Symbol值,不返回字符串属性名。
let obj = {}
let sym = Symbol()
obj.a = 'juejin'
obj[sym] = 'a symbol'
Object.getOwnPropertySymbols(obj) // [Symbol()]
Reflect.ownKeys,会返回一个数组,包含所有类型的键名
let obj = {
num:1,
[Symbol('sym')]:'juejin'
}
Reflect.ownKeys(obj) // ["num", Symbol(sym)]
(二)、用于非私有内部方法
因为Symbol值作为对象属性名时,不会被常规方法遍历到,所以,我们可以利用这个特性来定义一些非私有的内部方法。
var size = Symbol('size')
class Collection{
constructor(){
this[size] = 0
}
add(item){
this[this[size]] = item
this[size]++
}
static sizeOf(instance){
return instance[size]
}
}
var x = new Collection()
Collection.sizeOf(x) // 0
x.add('foo')
Collection.sizeOf(x) // 1
Object.keys(x) // ['0'] ,因为size属性名是Symbol值,所以无法获取
Object.getOwnPropertyNames(x) // ['0'] ,因为size属性名是Symbol值,所以无法获取
Object.getOwnPropertySymbols(x) // [Symbol(size)]
(三)、用作常量
假如有一个计算面积的函数,可以根据传入的参数,采用不同的计算公式,如下:
var shapeType = { triangle: 'Triangle',square: 'Square',...};
function getArea(shape, options) {
var area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
case shapeType.square:
area = options.width * options.height;
break;
...
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
仔细观察可以发现,shapeType对象里的键值是什么并不重要,我们只要保证他们不一样就可以了,这种情况就非常适合采用Symbol。
var shapeType = { triangle: Symbol(), square: Symbol(),...};
所以,当某个场景我们仅仅需要不相同的变量时,而不在乎变量的值是什么,这时候我们就可以用Symbol。
扩展篇
运用上述的知识,我们可以定义自己的Symbol值。除此之外,ES6还提供了11个内置的Symbol值,这些值其实与上述的Symbol知识点关系不大,我们可以把他们理解为JavaScript中特定的值,他们代表着某个方法或某个值。我挑选其中两个工作中常用的介绍下,其他的可以参考MDN。
一、Symbol.hasInstance
Symbol.hasInstance是对象的一个属性,它指向一个方法,对象使用instanceof运算符时会调用这个方法。也就是说,当我们使用arr instanceof Array时,在语言内部实际上是调用了Array[Symbol.hasInstance] (arr) 这个方法,来去检测arr是否是Array的实例。既然instanceof的本质是Symbol.hasInstance方法,那么假如我们改写一个构造函数的Symbol.hasInstance方法,就可以改变instanceof的行为:
class Test{
static [Symbol.hasInstance](param){
return param === 'juejin'
}
}
'github' instanceof Test // false
'juejin' instanceof Test // true
二、Symbol.species
Symbol.species是对象的属性,它指向当前对象的构造函数。当创建衍生对象时,会调用这个方法。也就是说,把调用这个函数后返回的函数当做构造函数,来创建衍生对象。默认的Symbol.species属性,会返回当前构造函数本身。请看下面这个例子:
class MyArray extends Array {
// 默认的Symbol.species方法等同于下面的写法,Symbol.species采用了get读取器,避免被修改
static get [Symbol.species](){ //
retrun this
}
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
// 在创建b和c时,实际上是使用了MyArray内部的Symbol.species方法返回的this当做构造函数,创造了b和c
b instanceof MyArray // true
c instanceof MyArray // true
我们可以覆盖对象原有的Symbol.species属性,这样可以改变一些原有的行为,产生特殊的效果:
class myArray extends Array{
static get [Symbol.species](){
return Array // 把构造函数改写成Array
}
}
var arr = new myArray(1,2,3)
var mapArr = arr.map(x => x); // 创建衍生对象mapArr
mapArr instanceof MyArray // false,因为Symbol.species已经被修改,所以mapArr不是MyArray的实例
mapArr instanceof Array // true
参考资料:
《ES6标准入门》_阮一峰
Symbol — MDN