logo头像
游魂的网络日志

初识JS的Symbol数据类型以及它的使用场景

初识 symbol

什么是 Symbol

Symbol作为ES6 新增原始数据类型的一种,表示独一无二的值。

回忆一下原始类型的范畴(string, number, boolean, null, undefined, symbol)。

Symbol的使用

创建一个Symbol

1
2
const a = Symbol()
console.log(typeof a) // symbol

需要注意的是通过 Symbol 方法创建值的时候不用使用 new 操作符,原因是通过 new 实例化的结果是一个 object 对象,而不是原始类型的 symbol。

1
2
const a = new Symbol()
console.log(typeof a) // Symbol is not a constructor

通常使用new来构造是想要得到一个包装对象,而Symbol不允许这么做,那么如果我们想要得到一个Symbol()的对象形式,可以使用Object()函数。

1
2
3
const a = Symbol()
const b = Object(a)
console.log(typeof b) // object

Symbol 方法接收一个参数,表示对生成的 symbol 值的一种描述。

1
2
const a = Symbol('a')
const b = Symbol('b')

即使是传入相同的参数,生成的 symbol 值也是不相等的,因为 Symbol 本来就是独一无二的意思。

1
2
3
4
const a = Symbol('foo')
const b = Symbol('foo')

console.log(a === b) // false

Symbol的应用

Symbol的应用其实利用了唯一性的特性。

作为对象的属性

大家有没有想过,如果我们在不了解一个对象的时候,想为其添加一个方法或者属性,又怕键名重复引起覆盖的问题,而这个时候我们就需要一个唯一性的键来解决这个问题,于是Symbol出场了,它可以作为对象的属性的键,并避免冲突。

1
2
3
4
5
6
7
8
// 创建一个`Symbol`
const a = Symbol()

// 创建一个对象
const obj = {}

// 通过`obj[]`将`Symbol`作为对象的键
obj[a] = 'hello world'

值得注意的是我们无法使用.来调用对象的Symbol属性,所以必须使用[]来访问Symbol属性

降低代码耦合

代码千万行,维护第一难。编码不规范,同事两行泪。

当代码中充斥着大量的魔法字符时,纵使是原开发者在经过一段时间后再回头看也会变得难以理解,更不必说是交由后来开发者维护。

假如现有一个 Tabs 切换的功能:

1
2
3
4
5
6
7
if (type === 'basic') {
return <div>basic tab</div>
}

if (type === 'super') {
return <div>super tab</div>
}

上面代码中字符串 basic、super 就是与业务代码无关的魔法字符,接下来使用 Symbol 对这块代码进行改造。

1
2
3
4
5
6
7
8
9
10
11
12
const tabTypes = {
basic: Symbol(),
super: Symbol(),
}

if (type === tabTypes.basic) {
return <div>basic tab</div>
}

if (type === tabTypes.super) {
return <div>super tab</div>
}

模拟类的私有方法

ES6 中的类是没有 private 关键字来声明类的私有方法和私有变量的,但是我们可以利用 Symbol 的唯一性来模拟。

1
2
3
4
5
6
const speak = Symbol()
class Person {
[speak]() {
...
}
}

因为使用者无法在外部创建出一个相同的 speak,所以就无法调用该方法。

全局共享Symbol

如果我们想在不同的地方调用已经同一Symbol即全局共享的Symbol,可以通过Symbol.for()方法,参数为创建时传入的描述字符串,该方法可以遍历全局注册表中的的Symbol,当搜索到相同描述,那么会调用这个Symbol,如果没有搜索到,就会创建一个新的Symbol

为了更好地理解,请看下面例子

1
2
3
const a = Symbol.for('a')
const b = Symbol.for('a')
a === b // true

如上创建Symbol

  1. 首先通过Symbol.for()在全局注册表中寻找描述为aSymbol,而目前没有符合条件的Symbol,所以创建了一个描述为aSymbol
  2. 当声明b并使用Symbol.for()在全局注册表中寻找描述为aSymbol,找到并赋值
  3. 比较ab结果为true反映了Symbol.for()的作用

再来看看下面这段代码

1
2
3
const a = Symbol('a')
const b = Symbol.for('a')
a === b // false

结果竟然是false,与上面的区别仅仅在于第一个Symbol的创建方式,我们来一步一步分析一下为什么会出现这样的结果。

  1. 使用Symbol('a')直接创建,所以该Symbol('a')不在全局注册表中
  2. 使用Symbol.for('a')在全局注册表中寻找描述为aSymbol,并没有找到,所以在全局注册表中又创建了一个描述为a的新的Symbol
  3. 秉承Symbol创建的唯一特性,所以ab创建的Symbol不同,结果为false

问题又又又来了!我们如何去判断我们的Symbol是否在全局注册表中呢?

Symbol.keyFor()帮我们解决了这个问题,他可以通过变量名查询该变量名对应的Symbol是否在全局注册表中(Symbol.for创建的)

1
2
3
4
5
// Symbol.keyFor 方法返回一个使用 Symbol.for 方法创建的 symbol 值的 key
const a = Symbol('a')
const b = Symbol.for('a')
Symbol.keyFor(a) // undefined
Symbol.keyFor(b) // 'a'

内置Symbol值又是什么?

上面的Symbol使用是我们自定义的,而JS又内置了Symbol值,个人的理解为:由于唯一性特点,在对象内,作为一个唯一性的键并对应着一个方法,在对象调用某方法的时候会调用这个Symbol值对应的方法,并且我们还可以通过更改内置Symbol值对应的方法来达到更改外部方法作用的效果。

为了更好地理解上面这一大段话,咱们以Symbol.hasInstance作为例子来看看内置Symbol到底是个啥!

1
2
3
4
5
6
class demo {
   static [Symbol.hasInstance](item) {
       return item === '游魂博客'
  }
}
"游魂博客" instanceof demo // true

Symbol.hasInstance对应的外部方法是instanceof经常用于判断类型。上面代码创建了一个demo类,并重写了Symbol.hasInstance,所以其对应的instanceof行为也会发生改变,其内部的机制是这样的:当我们调用instanceof方法的时候,内部对应调用Symbol.hasInstance对应的方法即return item === '游魂博客'

更多内置属性查看:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#static_properties

obj中Symbol key的获取

es6针对这个,添加了Object.getOwnPropertySymbols方法。

1
2
3
4
5
6
7
8
let uid = Symbol('uid')
let obj = {
[uid]: 'uid'
}

console.log(Object.keys(obj)) // []
console.log(Object.getOwnPropertyNames(obj)) // []
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(uid)]

Symbol 不可强制转换

1
2
let uid = Symbol('uid')
uid + ''

这里会报错,根据规范,他会把uid转换成字符串进行相加。如果真的相加,可以先String(uid)之后再相加,不过目前看来,似乎没什么意义。

这里只是介绍了Symbol的一些基础用法,其他使用请参考文档:MDN