前言
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并理解它,或许能在你设计并实现一些逻辑时,为你提供一种解决思路。
如果文章对你有帮助,顺手点个赞和关注吧~