Skip to content

26. Symbol 内置属性的妙用

阅读本文之前,先来看看一个问题。

js
const obj = {}
// 定义 obj,使得下列正确打印出相应的结果
console.log(obj * 10)       // 114514
console.log(`${obj} world`) // 'hello world'
console.log(obj + ' WORLD') // 'HELLO WORLD'
1
2
3
4
5

第一反应是不是想改写 valueOf 和 toString 方法?可以试试。

上述方法是不能满足需求的。一个正确的思路是使用 Symbol 的内置属性 Symbol.toPrimitive

下面来谈谈 Symbol 的几个内置属性。

1. Symbol.toPrimitive

Symbol.toPrimitive 是一个内置属性,确切的来说是一个方法,用来定义 对象试图转换为原始值的行为。引起这里 “试图试图转换为原始值” 的操作包括但不限于:

  • String(obj):对象 -> 字符串
  • Number(obj):对象 -> 数字
  • 对象与原始值相加:对象 + 原始值 -> 字符串
  • 对象与数字相乘:对象 * 数字 -> 数字
  • 对象与数字相除:对象 / 数字 -> 数字

Symbol.toPrimitive(type) 可以接收一个参数,表示要转换的类型,可能的值有三种:

  • string:表示转换为字符串
  • number:表示转换为数字
  • default:表示其他情况

使用格式:

js
cconst obj = {
  [Symbol.toPrimitive](type) {
    switch (type) {
      case 'number':
        // 返回数字
      case 'string':
        // 返回字符串
      default:
        // 返回原始值
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

那么开头的问题就不难解决了:

js
const obj = {
  [Symbol.toPrimitive](type) {
    switch (type) {
      case 'number':
        return 11451.4
      case 'string':
        return 'hello'
      default:
        return 'HELLO'
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

或者你可以使用表驱动:

js
const obj = {
  [Symbol.toPrimitive](type) {
    return new Map()
      .set('number', 11451.4)
      .set('string', 'hello')
      .set('default', 'HELLO')
      .get(type)
  }
}

console.log(obj * 10)       // 114514
console.log(`${obj} world`) // 'hello world'
console.log(obj + ' WORLD') // 'HELLO WORLD'
1
2
3
4
5
6
7
8
9
10
11
12
13

2. Symbol.hasInstance

Symbol 内置属性 Symbol.hasInstance 用来判断某个对象是否为某构造器的实例,可以用来改写 instanceof 运算符在某个类上的行为。

例如,编写一个判断奇数的类

js
class Odd {
  static [Symbol.hasInstance](obj) {
    return obj & 1
  }
}

console.log(1 instanceof Odd) // true
console.log(2 instanceof Odd) // false
console.log(3 instanceof Odd) // true
1
2
3
4
5
6
7
8
9

3. Symbol.iterator

Symbol 内置属性 Symbol.iterator 用来定义对象的可迭代性,而可迭代性服务于 for...of 循环、Array.from、数组扩展运算符等操作。因此,我们可以通过这个属性修改对象的迭代行为,使得不可迭代的对象可以变为可迭代,并且按照我们需要的方式进行迭代。

给数字添加可迭代性:

js
Number.prototype[Symbol.iterator] = function* () {
  yield* Array.from({ length: +this }, (_, i) => i + 1)
}

// 扩展运算符
console.log([...5]) // [1, 2, 3, 4, 5]

// Array.from
console.log(Array.from(5)) // [1, 2, 3, 4, 5]

// for ... of
for (let n of 5) {
  console.log(n) // 1 2 3 4 5
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

给对象添加可迭代性:

js
Object.prototype[Symbol.iterator] = function* () {
  yield* Object.entries(this)
}

const obj = { a: 1, b: 2, c: 3 }
for (const [key, value] of obj) {
  console.log(key, value)
}
1
2
3
4
5
6
7
8

4. Symbol.asyncIterator

Symbol 内置属性 Symbol.asyncIteratorSymbol.iterator 类似,但是用来 定义对象的异步迭代器,可以定义一个异步迭代对象以及其行为。它定义的异步迭代对象 只可供 for await...of 运算符使用,但不可用于数组扩展运算符和 for ... of

js
const asnycRange = (from = 1, to = 5) => ({
  from: from,
  to: to,
  [Symbol.asyncIterator]: async function* () {
    for (let i = this.from; i <= this.to; i++) {
      await new Promise(resolve => setTimeout(resolve, 1000))
      yield i
    }
  }
})
1
2
3
4
5
6
7
8
9
10

上述代码定义了一个异步对象生成函数,每隔一秒打印一个数字,使用 for await ... of 运算符进行迭代。

js
!(async () => {
  for await (let i of asnycRange(1, 3)) {
    console.log(i)
  }
})()
// 每隔一秒依次打印 1 2 3
1
2
3
4
5
6

迭代器 vs 异步迭代器

所谓迭代器就是可迭代对象(iterator),分为一般迭代器和异步迭代器。

比较项迭代器异步迭代器
设置迭代器的方法Symbol.iteratorSymbol.asyncIterator
next 方法返回值anyPromise
迭代访问方法for .. offor await ... of

异步迭代器除了能使用 for await ... of 进行循环访问外,不具有一般迭代器的性质。

5. Symbol.toStringTag

Symbol 内置属性 Symbol.toStringTag 用来定义对象的字符串标签,用于描述对象的类型,可以用来改写 Object.prototype.toString.call 检测数据类型的方法。

js
const myObj = {
  [Symbol.toStringTag]: 'MyObject'
}

console.log(myObj.toString()) // '[object MyObject]'
console.log(Object.prototype.toString.call(myObj)) // '[object MyObject]'
1
2
3
4
5
6

6. Symbol.species

Symbol 内置属性 Symbol.species 是函数值属性,用来定义被扩展的对象的构造函数。

先来看一个不使用 Symbol.species 的例子:

js
class MyArray extends Array {
  double() {
    return this.map(e => 2 * e)
  }
}

const arr = new MyArray(1, 2, 3) // [1, 2, 3]
const double1 = arr.double()     // [2, 4, 6]
const double2 = double1.double() // [4, 8, 12]
// double1 可以继续使用 double 方法
double1 instanceof MyArray       // true
double1.constructor === MyArray  // true
1
2
3
4
5
6
7
8
9
10
11
12

这里的 arr 使用 MyArray 作为构造函数(类实质上也是构造函数的语法糖)来创建新数组,具体表现为 double1 继续可以使用 double() 方法生成新数组,说明此时 double1 的构造函数为 MyArray。

关键理解

但有时候我们自己写的项目或者开源库中的扩展的数据类型,有些方法只是供项目内使用,并不想被用户使用(比如这里的 double 方法),就可以使用 Symbol.species 来定义内建方法(map、filter 以及自定义的一些方法)返回的数据的构造函数。

js
class MyArray extends Array {
  double() {
    return this.map(e => 2 * e)
  }
  static get [Symbol.species]() {
    return Array
  }
}

const arr = new MyArray(1, 2, 3) // [1, 2, 3]
const double1 = arr.double() // [2, 4, 6]
const double2 = double1.double() // 报错,double1 没有此方法
// double1 构造函数为 Array,无法继续使用 double 方法
console.log(double1 instanceof MyArray) // false
console.log(double1 instanceof Array) // true
console.log(double1.constructor === Array) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

结合上述两种情况更好理解 Symbol.species 的作用,这一点在 MDN 上没讲清楚。同样的,Symbol.species 也适用于 MapSet,当需要扩展属性和方法的时候。

更多其他 Symbol 属性:MDN Symbol