Symbol公认符号

Symbol的公认符号

经典面试题起手😀

1
2
3
4
let girlfriend={
name:’肖鹿😍’,
age:18
}

要实现以下效果:

1
2
console.log([...pro]) 
// 输出 ['肖鹿😍', 18]

咱们知道 [...arg] 这种展开方法只能对 可迭代对象 使用,然而可迭代对象只包括 Map, Set, Array, WeakMap,那么如何让一个普通对象变得可迭代呢?这就要用到咱们今天说的公认符号啦☑️


1. 趁热打铁,Symbol.iterator 和 Symbol.asyncIterator😏

这一刻也没有为可迭代对象哀悼,即将赶到战场的是 Symbol.iterator, 它的作用就是让一个普通对象转变为一个可迭代对象, 让我们来看看它是怎么实现的吧🤩

就比如说咱们最熟悉的 map 方法,当我们 map 的时候,iterator起到了什么作用呢? 让我们来模拟一个简单的 map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function map(ite, f) {
// 生成迭代器
let iterator = ite[Symbol.iterator]()
return {
// 迭代器自身也迭代,更符合逻辑
[Symbol.iterator]() {
return this;
}
// 迭代器必须有一个next()方法
next() {
let res = iterator.next()
if(res.done) {
return res
} else {
return {value: f(res.value)}
}
}
}
}
1
在 `for/of` 循环里,会先调用 `symbol.iterator` 生成迭代器,之后一直调用 `next()` 方法,直到返回的 `done` 为 `true`😎

现在再看那道面试题,是不是就迎刃而解了😗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let girlfriend= {
name: '肖鹿😍',
age: 18,
[Symbol.iterator]: function () {
const that = this
let count = 0
return {
next() {
return count < 2 ? {value:that[Object.keys(that)[count++]]} : {done: true}
},
[Symbol.iterator]() {return this}
}
}
}
console.log([...girlfriend])

这样其实还有点麻烦,可以和生成器结合😏, 由于生成器也是一个特殊的迭代器,所以可以将上面的代码简化为:

1
2
3
4
5
6
7
8
9
let girlfriend={
name:’肖鹿😍’,
age:18
*[Symbol.iterator](){
yield this.name;
yield this.age;
}
}

当时我看到这个的时候确实被震撼到了,原来还可以这么玩🌟

async版本的iterator功能更强大,可以异步控制迭代器的输出,但是这不是咱们今天的重点,感兴趣的话可以去看看🦏书的第12章末尾,有一个异步可迭代队列的例子


2. 定形数组的爹,Symbol.hasInstance😀

ES6 中,当我们使用 instanceof 的时候,会先调用 [Symbol.hasInstance]这个方法, 如果没有这个方法,则会采用 ES5 的方法,从原型链上查找它的prototype

这就可以自定义instanceof啦!

来看🌰:

1
2
3
4
5
6
7
8
let uint8 = {
[Symbol.hasInstance]() {
return Number.isInteger(x) && x >= 0 && x <= 256
}
}

128 instanceof uint8

3. Symbol.toStringTag

这个就厉害了, 在 ES6中, toString方法会先调用Symbol.toStringTag, 如果没有, 那就是经典的 [Object, Object]

1
2
3
4
5
6
7
8
9
class girlfriend {
[Symbol.toStringTag]= "肖鹿小姐😍"
}

// get [Symbol.toStringTag]() {return "肖鹿小姐😍"} 这样用getter定义更加专业,并且不会被枚举

let MyWife = new girlfriend()

console.log(MyWife.toString())

4. Symbol.species

这个方法比较冷门,但是确实实用,在 ES6 中, 继承的时候会把上一层的species也继承过来,map 以及 slice 等函数会先调用 new this.constructor[Symbol.species]() 创建新数组

比如:

1
2
3
4
5
6
7
8
9
10
class WifeArray extends Array {
static get [Symbol.species]() {return WifeArray}
get [Symbol.toStringTag]() {return WifeArray}
get first() {return this[0]}
get last() {return this[this.length-1]}
}

let e = new WifeArray('肖', '鹿', '😍')
let f = e.map(item => item + '🥰')
console.log(f)

5. Symbol.isConcatSpreadable🤐

如果是一个数组concat另一个数组,会发生什么?比如 [1][2],是会变成 [1,2], 还是 [[1], [2]] ?这就是咱们 isConcatSpreadable 发挥的地方了

1
2
3
4
5
6
7
let arraylike = {
length: 1,
0:1,
[Symbol.isConcatSpreadable]: false
}

[].concat(arraylike)

简单吧😏

6. 唯一真神,Symbol.toPrimitive🐶🐶🐶🐶🐶🐶🐶🐶

还是经典面试题起手

1
2
let a;
a == 1 && a == 2 && a == 3 什么时候成立?

除了经典做法,还可以使用Symbol.toPrimitive方法,为什么说它是唯一真神,就是因为它可以覆盖 valueOftoString 这两个幻神方法

使用 Symbol.toPrimitive 也很简单, 只不过要注意,与 valueOftoString 不同,它接受一个参数 hint, hint有三个取值: default, string, number

--"string":

当需要一个字符串表示的对象时,例如通过 String(obj) 进行转换或在某些与字符串相关的操作中(如调用 alert(obj))。
在这种情况下,JavaScript会尝试优先调用对象的 toString 方法(如果存在),然后尝试 valueOf 方法。

-- "number":

当需要一个数字表示的对象时,例如通过 Number(obj) 进行转换、算术运算(如 +obj 或 obj - 0)。
在这种情况下,JavaScript会尝试优先调用对象的 valueOf 方法(如果存在),然后尝试 toString 方法。

-- "default":

当操作不需要特定的类型(既不需要字符串也不需要数字),例如使用双等号 == 进行比较。
在这种情况下,多数情况下的行为与 "number" 类似,但并非总是如此。例如,使用二进制加法操作符 + 时,如果其中一个操作数是对象,那么该对象的 Symbol.toPrimitive 方法(如果存在)会被调用并传入 "default" 作为 hint。

因此,可以将上面的面试题改写成:

1
2
3
a = {
[Symbol.toPrimitive]: ((i) => () => ++i) (0)
}

这里的写法比较巧妙, 分为外层立即执行函数, 内层(i) => (), 最内层 () => ++i,因此会形成闭包,之后每一次调用都会使返回值递增


Symbol公认符号
http://baidu.com/2023/10/29/Symbol/
作者
KB
发布于
2023年10月29日
许可协议