# Symbol的应用场景

Symbol (符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。符号就是用来创建唯一记号,进而用作非 字符串形式的对象属性。

# 1、概述

Symbol 值通过Symbol函数生成。

Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,因此相同参数的Symbol函数的返回值是不相等的。

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

s1 === s2 // false

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

s1 === s2 // false
1
2
3
4
5
6
7
8
9
10
11

Symbol 值不能与其他类型的值进行运算,会报错。但是,Symbol 值可以显式转为字符串。

let sym = Symbol('My symbol');

"your symbol is " + sym
// TypeError: can't convert symbol to string
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
1
2
3
4
5
6

Symbol 值也可以转为布尔值,但是不能转为数值。

let sym = Symbol();
Boolean(sym) // true
!sym  // false
1
2
3

ES2019 提供了一个实例属性description,直接返回 Symbol 的描述。

const sym = Symbol('foo');

sym.description // "foo"
1
2
3

# 2、Symbol.prototype.description

创建 Symbol 的时候,可以添加一个描述。

const sym = Symbol('foo');

String(sym) // "Symbol(foo)"
sym.toString() // "Symbol(foo)"
1
2
3
4

ES2019 提供了一个实例属性description,直接返回 Symbol 的描述。

const sym = Symbol('foo');

sym.description // "foo"
1
2
3

# 3、作为属性名的 Symbol

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。

let mySymbol = Symbol();

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

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

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

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

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

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

let s = Symbol();

let obj = {
  [s](arg) { ... }
};

obj[s](123);
1
2
3
4
5
6
7

定义一组常量

const COLOR_RED    = Symbol();
const COLOR_GREEN  = Symbol();

function getComplement(color) {
  switch (color) {
    case COLOR_RED:
      return COLOR_GREEN;
    case COLOR_GREEN:
      return COLOR_RED;
    default:
      throw new Error('Undefined color');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 4、实例:消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

function getArea(shape, options) {
  let area = 0;

  switch (shape) {
    case 'Triangle': // 魔术字符串
      area = .5 * options.width * options.height;
      break;
    /* ... more code ... */
  }

  return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

常用的消除魔术字符串的方法,就是把它写成一个变量。

const shapeType = {
  triangle: 'Triangle'
};

function getArea(shape, options) {
  let area = 0;
  switch (shape) {
    case shapeType.triangle:
      area = .5 * options.width * options.height;
      break;
  }
  return area;
}

getArea(shapeType.triangle, { width: 100, height: 100 });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。

如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。

const shapeType = {
  triangle: Symbol()
};
1
2
3

# 5、属性名的遍历

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

但是,它也不是私有属性,有一个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);

objectSymbols
// [Symbol(a), Symbol(b)]
1
2
3
4
5
6
7
8
9
10
11

另一个新的 API,Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
//  ["enum", "nonEnum", Symbol(my_key)]
1
2
3
4
5
6
7
8

# 6、Symbol.for(),Symbol.keyFor()

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

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

s1 === s2 // true
1
2
3
4

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

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

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
1
2
3
4
5

# 7、内置的 Symbol 值

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。

# 7.1 Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。

# 7.2 Symbol.isConcatSpreadable

对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']


let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']

obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 7.3 Symbol.species

对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。

// example-1
class MyArray extends Array {
}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray // true
c instanceof MyArray // true

// example-1
class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

const a = new MyArray();
const b = a.map(x => x);

b instanceof MyArray // false
b instanceof Array // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 7.4 Symbol.match

对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

# 7.5 Symbol.replace

对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。

对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

# 7.7 Symbol.split

对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

# 7.8 Symbol.iterator

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]
1
2
3
4
5
6
7
8

# 7.9 Symbol.toPrimitive

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

  • Number:该场合需要转成数值
  • String:该场合需要转成字符串
  • Default:该场合可以转成数值,也可以转成字符串
let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 123;
      case 'string':
        return 'str';
      case 'default':
        return 'default';
      default:
        throw new Error();
     }
   }
};

2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 7.10 Symbol.toStringTag

对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。

// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"

// 例二
class Collection {
  get [Symbol.toStringTag]() {
    return 'xxx';
  }
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
1
2
3
4
5
6
7
8
9
10
11
12

# 8、总结

  1. 符号是原始值,且符号实例是唯一、不可变的。

  2. Symbol 值通过 Symbol 函数生成。

  3. Symbol() != Symbol()

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

  5. Symbol 值也可以转为布尔值,但是不能转为数值 Boolean(Symbol()) // true

  6. description 属性返回 Symbol 的描述

  7. Symbol 可以作为属性名

  8. 属性名的遍历

    • Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for...in、for...of 循环中
    • Object.getOwnPropertySymbols() 方法,可以获取指定对象的所有 Symbol 属性名
    • Reflect.ownKeys() 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
  9. 两个方法:

    • Symbol.for(),接受一个字符串作为参数,允许共用 Symbol 值
    • Symbol.keyFor() 返回一个已登记的 Symbol 类型值的 key
  10. 内置的 Symbol 值,除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值

    • Symbol.hasInstance 当其他对象使用 instanceof 运算符时调用
    • Symbol.isConcatSpreadable 表示该对象用于 Array.prototype.concat() 时,是否可以展开
    • Symbol.species 指向一个构造函数 static get [Symbol.species]() { return Array; }
    • Symbol.match 指向一个函数,当执行 str.match(myObject) 时,如果该属性存在,会调用它,返回该方法的返回值。
    • Symbol.replace 同上,String.prototype.replace 方法调用时
    • Symbol.search 同上,String.prototype.search 方法调用时
    • Symbol.split 同上,String.prototype.split 方法调用时
    • Symbol.iterator 返回该对象迭代器
    • Symbol.toPrimitive 指向一个方法。该对象被转为原始类型的值时,会调用这个方法
    • Symbol.toStringTag String.prototype.split 方法调用时返回,可以用来定制 [object Object][object Array] 中 object 后面的那个字符串。
上次更新: 4/10/2022, 10:26:50 AM