带你一文读懂Javascript中ES6的Symbol
前言
Symbol
这个特性对于很多同学来说,可能是在学习ES6特性的过程中,感到比较困惑的一个特性点。在大部分开发场景中,你可能根本用不到这个特性,但理解Symbol
各个属性和方法的作用和意义还是非常有必要的,在一些特定的场景中,你会发现它不可或缺。Symbol
内含的方法和属性非常多,本文仅对大概率会用到的一些讲解。
基础类型
对Javascript
属性的小伙伴都知道,Javascript
中有6大基础类型:Boolean
,BigInt
,undefined
,Number
,String
,Symbol
。可以看到,Symbol
是Javascript
中的基础类型之一。简单的去理解,Symbol
数据就是一个全局唯一的值,那全局唯一有什么用?当然是用于避免冲突啦~
我们先来看看Symbol
最简单的用法:
const symbolOne = Symbol();
console.log(symbolOne); // Symbol()
由于Symbol
是基础数据类型,所以我们不能用new的方式去创建Symbol
对象,只需要直接调用Symbol
即可。这里我们创建了一个简单的Symbol
对象,你也可以像下面这样给Symbol
传入一个description
,用来标识一个Symbol
:
const symbolOne = Symbol('foreverpx');
console.log(symbolOne); // Symbol('foreverpx')
设置这个标识在调试的时候非常有用,你可以通过不同的标识把Symbol
值区分开来。
多人协同开发中创建Symbol
的时候,description
是有可能重复的。如果出现不同地方都用同一个description
创建了Symbol
,那这两个Symbol
是不相等的,比如下面的比较:
const symbolOne = Symbol('foreverpx');
const symbolTwo = Symbol('foreverpx');
console.log(symbolOne === symbolTwo); // false
这种情况就会比较奇怪,原本只是想通过全局唯一的值解决冲突,不同的description
返回不同的唯一值。但相同的description
应该返回一样的Symbol
值才对。
其实,description
只是一个标签,用于更清晰的标识Symbol
以及用于调试。虽然传入的description
相同,但是并不意味着Symbol('foreverpx')
的结果与Symbol('foreverpx')
相同,它们是不同的对象。
Symbol
在我看来,Symbol
的主要用途,是用来标识唯一的对象属性。
怎么理解?我们已经使用字符串来标识对象属性了,为什么还需要通过Symbol
来标识呢?
我们来看下面的场景:
我们预先定义好两个const来标识用户在线与不在线的两个状态,比如:
const ONLINE = 'online';
const OFFLINE = 'offline';
接着写一个带有switch
的函数来处理两种情况的业务逻辑:
function onUserStatus(status){
switch(status){
case ONLINE:
//do something
return 'user online';
case OFFLINE:
//do something
return 'user offline';
default:
return 'unknown status'
}
}
onUserStatus(ONLINE); // 'user online'
上面的代码现在看上去没有什么问题。但某天需求加了一种connenting
的状态,在新增常量的时候,前端同学copy了上面的OFFLINE
的语句来修改,但只修改了常量名为CONNECTING
,而忘了修改值为connenting
,那代码就会变成下面这样:
const ONLINE = 'online';
const OFFLINE = 'offline';
const CONNECTING = 'offline';
在调用onUserStatus
时,结果就不对了,这显然是变量值冲突导致的:
function onUserStatus(status){
switch(status){
case ONLINE:
//do something
return 'user online';
case OFFLINE:
//do something
return 'user offline';
default:
return 'unknown status'
}
}
onUserStatus(CONNECTING); //user offline
接下来我们把上面的代码,用Symbol
来写写看:
const ONLINE = Symbol('online');
const OFFLINE = Symbol('offline');
const CONNECTING = Symbol('offline');
function onUserStatus(status){
switch(status){
case ONLINE:
//do something
return 'user online';
case OFFLINE:
//do something
return 'user offline';
default:
return 'unknown status'
}
}
onUserStatus(CONNECTING); //unknown status
通过Symbol
改写后,会发现即使在copy代码后忘了改值,也不会产生与上面同样的结果。由于每次调用Symbol
都会生成一个全局唯一的值,所以传入上方任何一个常量,都不会得到相同的结果。
我们再来看一个对象的例子:
const foreverpx = {
cnName: 'px'
}
这里定义了一个叫foreverpx
的Object
,其中有一个叫cnName
的属性。如果我们像这样用字符串来作为对象属性的key,那任何一个可以访问这个对象的地方,都可以改变它的值,比如: foreverpx.cnName = 'anthor'
。在某些情况下,我们不期望这样。
用Symbol
我们可以解决这个问题:
const cnName = Symbol('cnName');
const foreverpx = {
[cnName]: 'px'
}
在这种写法下,只有当调用者同时拿到了cnName
这个Symbol
,才能修改这个属性的值。
从上面两个例子可以看到,使用Symbol
在一些场景下可以让我们的程序更加健壮。
Symbol.for 与 Symbol.keyFor
前面讲了Symbol
,写下来讲讲它的两个静态方法Symbol.for
,Symbol.keyFor
。
不记得什么是静态方法了?静态方法也就是在类上定义的,不需要实例化即可调用的方法,比如:
class ForeverPx{
static getName(){
console.log('foreverpx')
}
}
ForeverPx.getName(); // foreverpx
Symbol
与这两个静态方法的区别是啥?还记得最开始的例子吗,同一个字符串创建的两个Symbol
,它的值是不相等的,Symbol.for
可以解决这个问题。
当Symbol.for
被调用时,它首先会判断传入的key
是否有被创建过,如果没有,则创建一个新唯一值。如果有,则返回之前的唯一值。
const symbolOne = Symbol.for('foreverpx')
const symbolTwo = Symbol.for('foreverpx')
console.log(symbolOne === symbolTwo); //true
所以,对于Symbol.for
来说,key
就是Symbol
的标识。而对于Symbol
来说,key
只是个简单的描述而已。
const symbolOne = Symbol.for('foreverpx')
const symbolTwo = Symbol('foreverpx')
console.log(symbolOne === symbolTwo); //false
console.log(symbolOne === Symbol.for('foreverpx')); //true
接下来是Symbol.keyFor
,其实这个方法是比较好理解的,从名字就能看出来,它是获取对应Symbol.for
创建的唯一值的key的:
const symbolOne = Symbol.for('foreverpx')
console.log(Symbol.keyFor(symbolOne)); //foreverpx
Symbol.iterator
到目前为止,上面讲的都是Symbol
的构造函数和方法,接下来开始介绍Symbol
的一些静态属性。首先要介绍的就是Symbol.iterator
了。
Symbol.iterator
从名字上就能看出,跟我们经常接触到的迭代器有很大的关系。在讲Symbol.iterator
之前,我们先来回顾下迭代器的概念。
什么是迭代器?迭代器就是能让你遍历并操作一个集合中的每一个元素的方法。在Javascript中,每一次循环都可以被称为迭代。你可以用for循环来遍历一个数据或者对象。如果一个对象的属性可以被诸如for of
等表达式遍历,则称这个对象是可迭代的。比如:
const foreverpx = [1,2,3];
for(let i of foreverpx){
console.log(num); //1,2,3
}
可以被迭代的对象,在其原型上都能找到Symbol.iterator
属性,可以在控制台看到
所以如果你想知道一个对象能否被迭代器迭代,那么可以通过图上的方式来查看。
你会发现,并不是只要是对象就能被迭代,Object本身就是不能被迭代的。可以被迭代的对象还有String
,Map
之类的。
既然Symbol.iterator
是某些对象原型上的一个属性的key值,那么如果我们调用它,会返回什么呢?
调用后返回了一个迭代器对象,里面包含了一个next
方法,是不是跟yield*
很像呢。从逻辑上来看,我们只要通过不停的调用这个对象的next
方法,就能依次迭代对象里面的属性了。我们来验证一下:
const foreverpx = [1,2,3];
const iterator = foreverpx[Symbol.iterator]();
iterator.next(); //{value: 1, done: false}
iterator.next(); //{value: 2, done: false}
iterator.next(); //{value: 3, done: false}
iterator.next(); //{value: undefined, done: true}
next
方法每次调用时,会返回一个对象,里面包含2个属性,value
表示当前被迭代的值,done
表示当前是否所有属性都已迭代完。
既然对象的Symbol.iterator
属性对应的是一个方法,那么我们改写对应的方法,重新赋值给它,就能改变该对象的迭代行为呢?同样,我们通过代码来验证一下:
const foreverpx = [1,2,3];
foreverpx[Symbol.iterator] = function(){
return {
obj: foreverpx,
index: 0,
next(){
const idDone = this.index === this.obj.length;
this.index ++;
if(this.obj[this.index] === 2){
return {value: this.obj[this.index]*2, done: isDone }
}else{
return {value: this.obj[this.index], done: isDone }
}
}
}
}
for (let item of foreverpx) {
console.log(item);
}
//1,4,3
刚才上面有提到,不是所有的对象都能被迭代,就比如Object
:
const foreverpx = {
name: 'px',
age: '18'
}
for(let item of foreverpx){
console.log(item);
}
这段代码执行完毕之后,我们会发现报错了,这确实如我们所预期的那样:
但我们可以通过Symbol.iterator
让它变得可以被迭代
const foreverpx = {
name: 'px',
age: '18'
}
foreverpx[Symbol.iterator] = function(){
return {
obj: foreverpx,
index: 0,
next(){
const idDone = this.index === this.obj.length;
this.index ++;
return {value: this.obj[this.index], done: isDone }
}
}
}
for (let item of foreverpx) {
console.log(item); //px, 18
}
另外,有些同学可能会搞混for of
和for in
的概念,这里对它们的差别不做过多的赘述,如果想了解更多,可以查看这篇文章for in 和for of的区别。
Symbol.search
我们在想要匹配某个正则在字符串中的位置时,有时候会使用下面的
方法来获取:
'foreverpx'.search('px'); // 7
上面代码返回了px
在foreverpx
字符串中出现的第一个位置。
当字符串的search
方法被调用时,也即是调用了Symbol.search
方法。
按照Symbol.iterator
的思路,我们同样可以通过改写Symbol.search
来覆盖字符串调用search时候的默认行为。
const str = 'foreverpx';
const reg = '/px/'
reg[Symbol.search] = function(str){
return 2020;
}
str.search(reg); //2020
接下来的几个Symbol属性也是同样的作用和用法。
Symbol.split
同理,在字符串调用split
方法的时候,也即是调用了Symbol.split
。我们同样可以改写字符串在调用split
方法时候的行为。
const str = 'foreverpx cjl';
const splitReg = / /; //空格
splitReg[Symbol.split] = function(str){
return ['px', 'cjl'];
}
str.split(splitReg); // ['px', 'cjl']
Symbol.toPrimitive
就跟它的名字一样,定义如何让一个对象变得原始、简单。
比如你有一个数组,你想让他变primitive
,你可能会这么做:
const arr = ['px', 'cjl'];
const prim = `${arr}`; //"px,cjl"
可以通过Symbol.toPrimitive
改写这个默认行为:
const arr = ['px', 'cjl'];
arr[Symbol.toPrimitive] = function() {
return `foreverpx`;
};
const prim = `${arr}`; //"foreverpx"
对于Object
亦可
const obj = {name: 'px'};
obj[Symbol.toPrimitive] = function() {
return `foreverpx`;
};
const prim = `${obj}`; //"foreverpx"
把原本的结果[object Object]
变成了foreverpx
。
总结
总的来说,Symbol
其实更像是提供了一个工具集,这个集合既能让你去生成一个全局唯一的值,也能在运行时去修改很多原始对象的默认行为,这些是在Symbol
出现之前是难以做到的。虽然在大部分情下,你可能很少会用到Symbol
,但学习Symbol
并理解它,或许能在你设计并实现一些逻辑时,为你提供一种解决思路。
如果文章对你有帮助,顺手点个赞和关注吧~