likes
comments
collection
share

✨ ES6之Symbol:让你的代码✨变得独一无二 ✨"

作者站长头像
站长
· 阅读数 14

以下内容均是参考ES6教程来进行详细解读的~ ES6教程

一、概述

要学习一个新的东西我们首先要了解它出现的原因以及作用,这样才能彻底的掌握它。在ES5中对象的属性名都是字符串,这样很容易造成属性名的冲突,从而出现后面的值覆盖前面的值的情况。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法,新方法的名字就有可能与现有方法产生冲突。如果有一种机制保证每个属性的名字都是独一无二的就好了,这样就从根本上防止了属性名的冲突,这就是ES6引入Symbold的原因!

Symbol是一种新的原始数据类型,表示独一无二的值。它属于Javascript语言的原生数据类型之一,其他数据类型是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt ==>用来表示任意精度的整数,因为Number类型只能表示有限范围内的整数,超过范围的整数会丢失经度或溢出,BigInt类型可以处理任意大小的整数,没有范围限制)、对象(Object ==> Object/Array/Function/Date/RegExp/Set/Map)。

Symbol值通过Symbol()函数生成,也就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型就都是独一无二的,可以保证不会与其他属性名产生冲突。

let s = Symbol();
console.log(typeof s) // "symbol"

上面代码中,变量s就是独一无二的值。typeof运算符的结果表明变量s是Symbol数据类型,而不是字符串之类的其他类型。

注意

  • Symbol()函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,而不是对象所以不能使用new命令来调用。
let s = new Symbol() // TypeError: Symbol is not a constructor
  • 由于Symbol值不是对象所以也不能添加属性,基本上它就是一种类似于字符串的数据类型。

参数

Symbol()函数可以接受一个字符串作为参数,表示对Symbol实例的描述。这主要是为了在控制台显示或者转为字符串时比较容易区分,还有就是在对象中的作为属性名的使用,这个会在后续说到!

let s1 = Symbol('foo');
let s2 = Symbol('bar');

console.log(s1); // Symbol(foo)
console.log(s2); // Symbol(bar)

上述代码中,s1s2是两个Symbol值,如果不加参数它们在控制台的输出都将是Symbol(),不利于区分。有了参数以后就等于为它们加上了描述,输出的时候就能够分清到底是哪一个值。

如果参数是一个对象,就会调用该对象的toString()方法将其转为字符串,然后生成一个Symbol值。

const obj = {
    toStirng(){
        return 'abc';
    }
}
const sym = Symbol(obj);
console.log(sym) // Symbol(abc)

注意:Symbol()函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol()函数的返回值是不相等的。

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

console.log(s1 === s2) //false 

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

console.log(s1 === s2); //flase

上面代码中,s1s2都是Symbol()函数的返回值,而且参数相同,但是它们是不相等的。事实上,如果调用100次Symbol(),会得到100个互不相等的值。

注意

  • Symbol值不能与其他类型的值进行运算,会报错。
let sym = Symbol('My Symbol');
console.log("your symbol is" + sym)
//TypeErrot: can't convert symbol to string
console.log(`your symbol is ${sym}`)
//TypeErrot: can't convert symbol to string
  • Symbol值可以显示转换为字符串。
let sym = Symbol('My symbol');
console.log(String(sym)) // 'Symbol('My symbol')'
console.log(sym.toString()) // 'Symbol('My symbol')'
  • Symbol值也可以转为布尔值,但是不能转为数字。
let sym = Symbol();
console.log(Boolean(sym)) // true
console.log(!sym) // false

console.log(Number(sym)) // TypeError
console.log(sym + 2) // TypeError

二、Symbol.prototype.description

前面说过,函数创建Symbol值时,可以用参数添加一个描述。

// 字符串foo就是这个sym值的描述
let sym = Symbol('foo');

但是读取这个描述需要先将Symbol显示转为字符串然后再对该字符串进行一些操作才能获取到描述的字符串:

let sym = Symbol('foo');
console.log(String(sym)) // "Symbol(foo)"
console.log(sym.toString()) //"Symbol(foo)"
// 然后需要对该字符串进行split等操作...
console.log(sym.split("(")[1].split(")")[0]) // foo

上面的用法不是很方便,ES6提供了一个Symbol值的实例属性description,直接返回Symbol值的描述。

const sym = Symbol('foo');
console.log(sym.description) // foo

三、作为属性名的Symbol

这是我认为比较重要的一个Symbol的应用场景~ 由于每一个Symbol值都是不相等的,这意味着只要Symbol值作为标识符用于对象的属性名,就能保证不出现同名的属性。这对于一个对象由于多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol]="Hello";

// 第二种写法
let a = {
    [mySymbol]:"Hello"
};

// 第三种写法
let a = {};
Object.defineProperty(a,mySymbol,{value:"Hello"});

//以上写法都会得到同样的结果
console.log(a[mySymbol]) // "Hello"

注意:Symbol值作为对象属性名时,不能使用点运算符。

const mySymbol = Symbol();
const a = {};
a.mySymbol="Hello";

console.log(a[mySymbol]) // undefined
console.log(a['mySymbol']) // "Hello"

上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串而不是一个Symbol值。

同理,在对象内部使用Symbol值定义属性时,Symbol值必须放在方括号之中。

let s = Symbol();
let obj = {
    [s]:function(arg){ ... }
}
//也可用增强的对象写法,更简洁一些
let obj = {
    [s](arg){ ... }
}
obj[s](123);

上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个Symbol值。

还有一点需要注意,Symbol值作为属性名时,该属性还是公开属性,而不是私有属性。

四、属性名的遍历

Symbol值作为属性名,在遍历对象的时候该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

const obj = {};
const foo = Symbol('foo');
obj[foo] = 'bar';
for(let key in obj){
    console.log(key) //无输出
}
console.log(Object.keys(obj)); // []
console.log(Object.getOwnPropertyNames(obj)); // []
console.log(JSON.stringify(obj)); // {}

有一个Object.getOwnPropertySymbols()的方法可以获取指定对象的所有Symbol属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的Symbol值。

const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'world';
const objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols); // [Symbol(a),Symbol(b)]

另一个新的API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和Symbol键名。(至于Reflect是个什么东西我想后面单出一个章节~)

let obj = {
    [Symbol('my_key')]:1,
    enum:2, // 模拟可遍历的属性
    nonEnum:3 // 模拟不可便利的属性
};
console.log(Reflect.ownKeys(obj)) // ["enum","nonEnum",Symbol(my_key)]

由于以Symbol值作为键名不会被常规方法遍历得到。我们可以利用这个特性为对象定义一些非私有的、但又希望只用于内部的方法。

五、Symbol.for()和Symbol.keyFor()方法

Symbol.for()

有时,我们希望重新使用同一个Symbol值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值,如果有,就返回这个Symbol值,否则就新建一个以该字符串为名称的Symbol值并将其注册到全局。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

console.log(s1 === s2); // true

上面代码中,s2s1都是Symbol值,但是它们都是由同样的参数Symbol.for()方法生成的,所以实际上是同一个值。

Symbol.for()和Symbol()的区别

Symbol.for()Symbol()这两种写法都会生成新的Symbol。它们的区别是:

  1. 前者会被登记在全局环境中供搜索,后者不会;
  2. Symbol.for()不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。 比如,如果调用了Symbol.for("cat")30次,每次都会返回同一个Symbol值,但是调用Symbol("cat")30次,会返回30个不同的Symbol值。
Symbol.for("bar") === Symbol.for("bar") // true
Symbol("bar") === Symbol("bar) // false

注意,Symbol.for()为Symbo值登记的名字是全局环境的,不管有没有在全局环境下运行。

function foo(){
    return Symbol.for("bar");
};

const x = foo();
const y = Symbol.for("bar");
console.log(x === y); // true

上面代码中,Symbol.for('bar')是函数内部运行的,但是生成的Symbol值是登记在全局环境的,所以第二次运行Symbol.for('bar')可以取到这个Symbol值。

Symbol.keyFor()

Symbol.keyFor()方法返回一个已登记的Symbol类型值的key

let s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)) // "foo"

// 变量s2属于未登记的Symbol值,所以返回undefined
let s2 = Symbol("foo");
console.log(Symbol.keyFor(s2)) // undefined

内置的Symbol值

除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。这里就先不详细介绍了,感兴趣的同学可以去官网看看这一章节,内置的Symbol值其中我认为比较重要的一个知识点就是interator属性,因为它涉及到遍历器,有时间我也会单独把它抽出来做详细总结的。

最后希望对大家有帮助啦!

转载自:https://juejin.cn/post/7258207459358556197
评论
请登录