Appearance
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
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
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
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
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
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
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
2
3
4
5
6
7
8
4. Symbol.asyncIterator
Symbol 内置属性 Symbol.asyncIterator
和 Symbol.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
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
2
3
4
5
6
迭代器 vs 异步迭代器
所谓迭代器就是可迭代对象(iterator),分为一般迭代器和异步迭代器。
比较项 | 迭代器 | 异步迭代器 |
---|---|---|
设置迭代器的方法 | Symbol.iterator | Symbol.asyncIterator |
next 方法返回值 | any | Promise |
迭代访问方法 | for .. of | for 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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
结合上述两种情况更好理解 Symbol.species
的作用,这一点在 MDN 上没讲清楚。同样的,Symbol.species 也适用于 Map
和 Set
,当需要扩展属性和方法的时候。
更多其他 Symbol 属性:MDN Symbol