一、symbol是什么
symbol是ES6出的一种基本数据类型,通过Symbol函数返回symbol类型的值,并且每个从Symbol函数返回的symbol值是唯一的
如何确定数据类型是不是symbol:
console.log(typeof Symbol()) // symbol
console.log(Object.prototype.toString.call(Symbol())) // [object Symbol]
二、声明symbol的方式
1、第一种方式:直接调用Symbol函数创建唯一的symbol
语法:
Symbol(description) // description是可选的字符串,仅用于调试,传入的数据会被隐式转换为string
如果description是一个对象,会调用对象中的Symbol.toPrimitive(将对象转换为基本数据类型)方法,如果没有就会调用toString方法,如果这两个方法都没有,那么description就是一个object类型:
const obj = {
[Symbol.toPrimitive]() {
return 'Symbol.toPrimitive'
},
toString() {
return 'toString'
}
}
const symbol = Symbol(obj)
const symbol1 = Symbol({})
console.log(symbol.description) // Symbol.toPrimitive
console.log(symbol1.description) // [object Object]
Symbol的创建类似于内置的对象类,但是它不是构造函数,不支持new Symbol(),直接调用Symbol函数便可以得到一个symbol。Symbol函数中的参数是一个描述,可以通过toString/description得到这个描述:
const symbol1 = Symbol()
const symbol2 = Symbol(42)
const symbol3 = Symbol('foo')
console.log(symbol1, symbol1.toString(), symbol1.description) // Symbol() 'Symbol()' undefined
console.log(symbol2, symbol2.toString(), symbol2.description) // Symbol(42) 'Symbol(42)' '42'
console.log(symbol3, symbol3.toString(), symbol3.description) // Symbol(foo) 'Symbol(foo)' 'foo'
需要知道的是,每次调用Symbol函数都会生成一个新的symbol:
console.log(Symbol() === Symbol()) // false
console.log(Symbol('f') === Symbol('f')) // false
2、第二种方式:使用Symbol.for()定义全局的symbol
当你使用Symbol.for()去定义symbol,此时的symbol已经被保存为全局的,当再次使用相同的description,在内存中访问的是同一个symbol:
const symbol1 = Symbol.for('xx描述')
let symbol2
const fn = () => {
symbol2 = Symbol.for('xx描述')
}
fn()
console.log(symbol1 === symbol2) // true
对于Symbol.for()定义的symbol,通过Symbol.keyfor()获取到该全局symbol的描述:
console.log(Symbol.keyFor(Symbol()), typeof Symbol.keyFor(Symbol())) // undefined 类型是undefined
console.log(Symbol.keyFor(Symbol.for())) // 'undefined' 类型是string
console.log(Symbol.keyFor(Symbol.for(12))) // '12' 类型是string
三、Symbol的属性和方法
1、Symbol.iterator,被for...of使用
Symbol.iterator是for...of循环的底层机制,可以用Symbol.iterator来判断一个数据类型是否可以被for...of遍历到:
console.log([][Symbol.iterator]) // ƒ values() { [native code] }
console.log(new Set()[Symbol.iterator]) // ƒ values() { [native code] }
console.log(new Map()[Symbol.iterator]) // ƒ entries() { [native code] }
console.log(''[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] }
const labels = document.querySelectorAll('*') // 类数组
console.log(labels[Symbol.iterator]) // ƒ values() { [native code] }
console.log({}[Symbol.iterator]) // undefined
console.log(new Number()[Symbol.iterator]) // undefined
console.log(new Boolean()[Symbol.iterator]) // undefined
数组/Set/Map/类数组/字符串可以被for...of遍历,对象/数字/布尔值不可以被for...of遍历。for...of循环就是调用该数据结构的[Symbol.iteartor]函数,这个函数安装Iteartor迭代器规范设计的,手动实现迭代器:
Iteartor迭代器规范:
- 每一次循环都执行一次next函数
- next函数返回一个对象,对象中两个属性,一个是done表示循环是否结束,一个是value表示值
const arr = ['a', 'b', 'c']
arr[Symbol.iterator] = function () {
var _this = this
var index = 0
return {
next: function () {
if (index > _this.length - 1)
return { done: true, value: undefined }
return { done: false, value: _this[index++] }
}
}
}
for (const item of arr) {
console.log(item)
}
2、Symbol.asyncIterator,被for await of使用
for await of在执行时实际上是调用该数据的[Symbol.asyncIterator]函数,底层使用生成器函数实现的
const fn1 = () =>
new Promise((res) => {
setTimeout(() => {
res(1)
}, 1000)
})
const fn2 = () =>
new Promise((res) => {
res(2)
})
const fn3 = () =>
new Promise((res) => {
setTimeout(() => {
res(3)
}, 1000)
})
const test = async () => {
const obj = {}
obj[Symbol.asyncIterator] = async function* () {
yield fn1()
yield fn2()
yield fn3()
}
for await (const item of obj) {
console.log(item)
}
/*
依次打印1 2 3
*/
}
test()
3、Symbol.hasInstance,被instanceof使用
当使用instanceof时,底层调用的是类的静态方法[Symbol.hasInstance]
class F {
constructor() {
this.x = Symbol.for('x')
}
}
const f = new F()
console.log(f instanceof F) // true 毋庸置疑
const arr = ['a', 'b', 'c']
console.log(arr instanceof F) // false 毋庸置疑
Object.setPrototypeOf(arr, F.prototype) // 相当于arr.__proto__ = F.prototype
console.log(arr instanceof F) // true 因为已经将F的原型赋值给arr的__proto__了
如果不希望强行改变__proto__的指向来影响instanceof的结果,可以改写[Symbol.hasInstance]方法:
static [Symbol.hasInstance](obj) {
return obj.x && obj.x === Symbol.for('x')
}
4、Symbol.toPrimitive,将对象转换为基本数据类型
当使用==来比较对象和原始值时,首先会调用对象中的[Symbol.toPrimitive]方法,参考对象和原始值的比较
5、Symbol.toStringTag,被Object.prototype.toString()使用
当创建自己的类时,js默认采用Object标签:
function F() {}
const f = new F()
console.log(Object.prototype.toString.call(f)) // [object, Object]
可以通过[Symbol.toStringTag]设置自己的自定义标签:
F.prototype[Symbol.toStringTag] = 'F'
console.log(Object.prototype.toString.call(f)) // [object, F]
四、对象中symbol属性的迭代
1、for...in不会遍历到symbol类型的属性
const password = Symbol('password')
const obj = {
name: 'xx',
age: 18,
[password]: '123456'
}
for (const key in obj) {
console.log(key) // 只会打印2个键:name age
}
2、Object.key()仅获取对象自身非symbol类型的私有属性
const arr = Object.keys(obj)
console.log(arr) // ['name', 'age']
3、Object.getOwnPropertyNames()也无法获取symbol类型的属性
const names = Object.getOwnPropertyNames(obj)
console.log(names) // ['name', 'age']
4、JSON.stringify会自动过滤掉symbol类型的属性
console.log(JSON.stringify(obj)) // {"name":"xx","age":18} 当把一些数据存到localStorage中时,密码属于隐私数据,便可以采用symbol作为密码的键
5、Object.getOwnPropertySymbols()仅获取到对象中symbol类型的私有属性
const symbolList = Object.getOwnPropertySymbols(obj)
console.log(symbolList) // [Symbol(password)]
6、Reflect.ownKeys()获取对象中所有的私有属性
const all = Reflect.ownKeys(obj)
console.log(all) // ['name', 'age', Symbol(password)]
五、symbol的使用场景
1、使用symbol代替常量
(1)使用symbol代替id
比如说一个班级中所有的学生的分数,收集到一个对象中,假如有两个叫“张三”的同学,那么后一个会覆盖前一个:
const students = { 张三: 90, 张三: 100 }
console.log(students['张三']) // 100
我们可以变一下,通过id去存取分数:
const s1 = { name: '张三', id: 1 }
const s2 = { name: '张三', id: 2 }
const students = { [s1.id]: 90, [s2.id]: 100 }
console.log(students[s1.id]) // 90
console.log(students[s2.id]) // 100
以上id具体的值就可以使用symbol(Symbol函数会生成一个永不重复的值):
const s1 = { name: '张三', id: Symbol() }
const s2 = { name: '张三', id: Symbol() }
const students = { [s1.id]: 90, [s2.id]: 100 }
console.log(students[s1.id]) // 90
console.log(students[s2.id]) // 100
在这里id写成数字和写成Symbol()的效果是一样的,那好像用symbol麻烦一点呢?假如说数据量比较大,写100个呢,万一手滑了把两个id写成一样的,那不就gg了,但是每次调用Symbol()返回的值都是唯一的。
由于symbol的唯一性,所以symbol可以用来当做标识符使用,当使用symbol作为对象的键,那么对象中的键一定是唯一的。当我们想操作一个由多个模块构成的对象,向这个对象中添加某个属性时,可以用symbol作为键,避免对象中某个属性被修改或覆盖
const name = Symbol('name')
const obj = {...} // 这是一个非常复杂的对象
obj[name] = 'xxx' // 追加一个属性,使用symbol作为键一定不会对原属性造成冲突
在对象中,使用symbol存取值时,symbol值必须要放到方括号中,实例可以看下call方法的手动实现
(2)消除魔法字符串:使用symbol代替某个固定的字符串常量
啥叫魔法字符串?
const getBool = (val) => {
return val === 'name' ? true : false
}
getBool('name')
以上代码中的'name'就是魔法字符串,'name'如果使用多了,要更改这样的字符串就会比较麻烦,通常可以定义成一个常量:
const str = 'name'
const getBool = (val) => {
return val === str ? true : false
}
getBool(str)
你会发现这个常量是什么值并不重要,所以可以使用symbol:
const str = Symbol()
const getBool = (val) => {
return val === str ? true : false
}
getBool(str)
(3)vuex/redux中action-type.js中定义的常量
export const add = 'ADD'
export const minus = 'MINUS'
dispatch({
type: add,
...
})
类似这样的代码,就可以使用symbol代替字符串常量
export const add = Symbol()
export const minus = Symbol()
2、保护个别隐私属性
在对象中,如果键是symbol,使用for...in/Object.keys/Object.getOwnPropertyNames/JSON.stringify是获取不到的,对于一些不希望被别人通过常规方式遍历到的属性可以考虑使用symbol去定义