ES6之Symbol

1. Symbol的由来

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

2. Symbol的概述

JS中分为七种内置类型,七种内置类型又分为两大类型:基本类型、对象(Object)。基本类型有6种,分别是:null、undefined、boolean、number、string、Symbol。其中Symbol是ES6新添加的数据类型。

Symbol表示独一无二的。Symbol值通过Symbol函数生成。因此对象的属性名现在可以有两种类型,一种是我们原来一直都使用的字符串类型,另一种就是新增的Symbol类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。比如:
let s = Symbol();
此时变量s就是一个独一无二的值。当我们使用typeof运算符的时候,此时变量s是Symbol数据类型。
typeof s; // "symbol"

注意:我们在定义Symbol的时候,在Symbol函数前不能使用new命令,否则会报错。其原因是因为生成的Symbol是一个原始类型的值,而不是对象。由于Symbol值不是对象,因此不能添加属性。

3. Symbol的使用注意事项

  1. Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述。

    1
    2
    3
    4
    5
    let s1 = Symbol('foo');
    let s2 = Symbol('bar');

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

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

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

    1
    2
    3
    4
    5
    6
    7
    const obj = {
    toString() {
    return 'I am toString methods';
    }
    };
    const s = Symbol(obj);
    console.log(s); // Symbol(I am toString methods)
  3. 有参数与没有参数的情况:
    注意:Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

    1
    2
    3
    4
    5
    6
    7
    8
    // 没有参数的情况
    var s1 = Symbol();
    var s2 = Symbol();
    console.log(s1 === s2); // false
    // 有参数的情况
    var s1 = Symbol("foo");
    var s2 = Symbol("foo");
    console.log(s1 === s2); // false

    上面代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。

  4. Symbol 值不能与其他类型的值进行运算,会报错

    1
    2
    3
    let s = Symbol('foo');
    console.log("my symbol is " + s); // 报错,Uncaught TypeError: Cannot convert a Symbol value to a string
    console.log(`my symbol is ${s}`); // 报错,Uncaught TypeError: Cannot convert a Symbol value to a string

    需要注意的是:Symbol值可以显示转换为字符串。

    1
    2
    3
    let s = Symbol('foo');
    console.log(String(s)); // 'Symbol(foo)'
    console.log(s.toString()); // 'Symbol(foo)'

    我们还需要注意一点,Symbol 值也可以转为布尔值,但是不能转为数值。

    1
    2
    3
    4
    5
    6
    7
    let s = Symbol('foo');
    // 转化为布尔值
    console.log(Boolean(s)); // true
    console.log(!s); // false
    // 转化为数值
    console.log(Number(s)); // 报错,Uncaught TypeError: Cannot convert a Symbol value to a number
    console.log(s + 1); // 报错,Uncaught TypeError: Cannot convert a Symbol value to a number

4. Symbol作为属性名的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let mySymbol1 = Symbol("foo1");
let mySymbol2 = Symbol("foo2");
let mySymbol3 = Symbol("foo3");
// 第一种写法
let a1 = {};
a1[mySymbol1] = 'Hello!';
console.log(a1); // {Symbol(foo1): "Hello!"}
// 第二种写法
let a2 = { [mySymbol2]: 'Hello!' };
console.log(a2); // {Symbol(foo2): "Hello!"}
// 第三种写法
let a3 = {};
Object.defineProperty(a3, mySymbol3, { value: 'Hello!' });
console.log(a3); // {Symbol(foo3): "Hello!"}

注意:在这里不可以使用点运算符,因为点运算符后面总是字符串。:

1
2
3
4
let mySymbol4 = Symbol("foo4");
let a4 = {};
a4.mySymbol4 = 'hello';
console.log(a4); // {mySymbol4: "hello"}

5. 为什么要使用Symbol

说了这么久,那么我们到底为什么要使用Symbol呢?下面给大家一个场景:我们想区分两个属性,其实我们并不在意,这两个属性值究竟是什么,我们在意的是,这两个属性绝对要区分开来。比如:我们想计算不同形状的面积,由于不同形状需要用不同的计算面积的公式来进行相应的运算。当我们不使用Symbol时,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
let shape_type = {rectangle: "Rectangle"};
function showArea(shape,selects){
let area = 0;
switch(shape){
case shape_type.rectangle:
area = selects.width * selects.height;
break;
}
return area;
}
console.log(showArea(shape_type.rectangle,{width: 10,height: 10})); // 10

在这里使用rectangle的名字叫做”Rectangle”,其实真正意义上我们不想对rectangle去特地的去一个名字,我们只想要区分rectangle这个形状不同于任何其他形状,那么这个时候Symbol就派上用场了。

1
2
3
const shape_type = {
rectangle: Symbol()
};

也就是说,我们不用非要去给变量赋一个字符串的值,去区分它和别的变量的值不同,因为去给每个变量取个语义化而又不同的值是一件伤脑子的事,当我们只需要知道每个变量的值都是百分百不同的即可,这时候我们就可以用Symbol。