作为前端的你竟然还没用过Symbol!
The following article is from 前端界 Author 芝士
来源:前端界 (ID: gh_be76678b7c8b)
作者:芝士
本文你能学到?
Symbol 是什么
Symbol 是一种基本数据类型。Symbol()
函数返回 symbol
类型的值,
注意!注意!注意! 通过
Symbol
创建返回的symbol
值都是唯一的。一个symbol
值能作为对象属性的标识符;这是该数据类型仅有的目的。
创建 symbol
值的语法(注意没有 new
):
Symbol([description])
举个例子:
const symbol1 = Symbol();
const symbol2 = Symbol(123);
const symbol3 = Symbol('test');
console.log(symbol2); // Symbol(123)
console.log(typeof symbol1); // symbol
console.log(symbol2 === 123); // 这个永远输出是false
console.log(Symbol('test')===Symbol('test'));// 永远输出是false
可以看出通过 Symbol
创建返回的symbol
值都是唯一的。
Symbol 特点说明
这些特点可能会导致你在开发过程中出现 BUG
,需要知道下。
1. Symbol 不可以使用New创建
var symbol = new Symbol()
上面这种是有问题的创建 symbol
值方式,会抛出 TypeError
错误。
2. Symbol类型值需通过 Object.getOwnPropertySymbols() 获取
Symbol
创建的值是不可枚举的,以下方式遍历对象的结果都不会包含 symbol
内容
for in
循环:循环会遍历对象的可枚举属性,但会忽略不可枚举的属性Object.keys()
:方法返回一个数组,其中包含对象的所有可枚举属性的名称。不可枚举的属性不会被包含在返回的数组中JSON.stringify()
:只会序列化对象的可枚举属性,而不会包含不可枚举属性Object.assign()
:用于将源对象中可枚举属性复制到目标对象。不可枚举属性不会复制Object.getOwnPropertyNames()
:返回一个数组,其中包含对象的所有属性(包括不可枚举属性)的名称,但是不包括使用symbol
值作为名称的属性
可以将 Symbol
类型数据遍历出来的函数
Object.getOwnPropertySymbols
方法可以获取指定对象的所有Symbol
属性名Reflect.ownKeys
方法可以获取指定对象的所有Symbol
属性名
let symbol = Symbol('test');
let obj = {[symbol]:123};
for(const key in obj){
console.log(key); // 无打印信息
}
console.log(obj[symbol]); // 123
console.log(Object.keys(obj)); // []
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(test) ]
console.log(Reflect.ownKeys(obj)); // [ Symbol(test) ]
Object.assign
将属性从源对象复制到目标对象,会包含Symbol
类型作为key
的属性
const symbolKey = Symbol('key');
const source = {
[symbolKey]: 'Symbol Property',
regularProperty: 'Regular Property'
};
Object.defineProperty(source,"w",{
value:456,
enumerable:true,
configurable:true,
writable:true
})
Object.defineProperty(source,"r",{
value:123,
enumerable:false,
configurable:false,
writable:false
})
const target = {};
Object.assign(target, source);
console.log(target);
// 输出
// {
// regularProperty: 'Regular Property',
// w: 456,
// [Symbol(key)]: 'Symbol Property'
// }
// [Symbol(key)] 类型也会被打印,但是不可枚举属性不会打印
3. Symbol 在 JSON.stringify 中的问题
JSON.stringify
转换和 Symbol
相关的数据时,规则如下
JSON.stringify
的时候,如果对象中key
或者value
都是Symbol
类型时候。转换过程会把它忽略掉JSON.stringify
直接转换symbol
类型数据,转换后的结果为undefined
JSON.stringify
转换的是一个对象,无论key
还是value
中有symbol
类型,都会忽略掉
4. 对象中声明的 Symbol 属性获取方式
需要使用 Symbol
对象属性的时候,用 obj[]
,必须使用[]
方式获取属性
let symbol = Symbol('test');
let obj = {[symbol]:123};
console.log(obj[symbol]); // 123
5. Symbol.For 介绍
项目中如果想要使用同一个 Symbol
值,可以使用 Symbol.for
。它接受一个字符串作为参数,用 Symbol.for()
方法创建的 symbol
会被放入一个全局的 symbol
注册表。Symbol.for()
不是每次都创建新的 symbol
,会先搜索有没有以该参数作为名称的 Symbol
值。如果有,就返回这个 Symbol
值,否则就新建并返回一个以该字符串为名称的 Symbol
值。
console.log(Symbol.for('test') === Symbol.for('test')); // true
console.log(Symbol("test")===Symbol("test")); // false
6. Symbol 直接传入一个函数
使用 Symbol
直接传入一个函数,会调用 toString
函数,将函数内容转换为字符串
let a = function(){
console.log('哈哈哈');
}
console.log(Symbol(a)); // Symbol(function(){ console.log('哈哈哈');})
console.log(typeof Symbol(a)); // symbol
Symbol 应用场景
应用场景不止针对的是 symbol
这个基础的数据类型,还会针对 Symbol
中提供的一些函数,比如 Symbol.iterator
,Symbol.toStringTag
等
自定义迭代器之Symbol.iterator
使用 Symbol
实现普通对象迭代器,普通的对象是不支持迭代器功能的,也就是普通对象不能直接使用for of
功能,有了它我们如何让一个对象支持了遍历
Symbol.iterator
为每一个可遍历对象定义了默认的迭代器。该迭代器可以被for of
循环使用。Array,Map,Set,String
都有内置的迭代器。 但是普通对象是不支持迭代器功能的,也就不能使用 for of
循环遍历。 接下来我们使用 Symbol.iterator
实现一个可迭代对象
demo1(Symbol.iterator属性中使用next)
let symbolObjTest1 = {
0:"a",
1:"b",
2:"c",
length:3,
[Symbol.iterator]:function(){
let index = 0;
return {
next(){ // 迭代器返回的对象需要有next函数
return {
value:symbolObjTest1[index++], // value为迭代器生成的值
done:index>symbolObjTest1.length // 迭代器的终止条件,done为true时终止遍历
}
}
}
}
}
for(const iterator1 of symbolObjTest1){
console.log(iterator1); // 打印 a b c
}
demo2(Symbol.iterator属性中使用Generator)
let symbolObjTest2 = {
0:"d",
1:"e",
2:"f",
length:3,
[Symbol.iterator]:function*(){ // 注意Generator函数格式
let index = 0;
while(index<symbolObjTest2.length){
yield symbolObjTets2[index++]
}
}
}
for(const iterator2 of symbolObjTest2){
console.log(iterator2);//打印 d e f
}
demo3(不影响原始对象遍历,遍历正常返回key value)
const obj = {a:1,b:2,c:3};
obj[Symbol.iterator] = function*(){
for(const key of Object.keys(this)){
yield [key,this[key]]
}
}
for(const [key,value] of obj){
console.log(`${key}:${value}`); // 打印
}
总结一下,一个对象只要有
[Symbol.iterator]
属性,它就拥有了可迭代的能力。
demo4 将一个class对象实现支持迭代器
class Animal{
constructor(name,sex,isMammal){
this.name = name;
this.sex = sex;
this.isMammal = isMammal;
}
}
class Zoo{
constructor(){
this.animals = [];
}
addAnimals(animal){
this.animals.push(animal);
}
[Symbol.iterator](){
let index = 0;
const animals = this.animals;
return {
next(){
return {
value:animals[index++],
done:index>animals.length
}
}
}
}
}
const zoo = new Zoo();
zoo.addAnimals(new Animal('dog','victory',true));
zoo.addAnimals(new Animal('pig','defeat',false));
zoo.addAnimals(new Animal('cat','defeat',false));
for (const animal of zoo) {
console.log(`${animal.name};${animal.sex};${animal.isMammal}`)
}
// 打印 dog;victory;true pig;defeat;false cat;defeat;false
这个例子写完文章后感觉不太贴切,用动物园和动物进行举例,大家练习的时候可以把它改成实体和列表,更好理解
Symbol.toStringTag 使用
Symbol.toStringTag
官方描述是一个字符串值属性,用于创建对象的默认字符串描述。由 Object.property.toString()
方法内部访问
MDN
描述: Object.property.toString()
返回一个表示该对象的字符串。旨在重写(自定义)派生类对象的类型转换逻辑。
它在我开发过程中最常用场景是判断类型
举个例子:
const toStringCallFun = Object.prototype.toString.call;
toStringCallFun(new Date); // [object Date]
toStringCallFun(new String); // [object String]
toStringCallFun(Math); // [object Math]
toStringCallFun(undefined); // [object Undefined]
toStringCallFun(null); // [object Null]
我们也可以自定义函数,来覆盖默认的 toString()
方法。自定义的 toString()
方法可以是任何你想要的值。
举个例子:
function Student(score,age){
this.score = score;
this.age = age;
}
let student = new Student('100',13);
// 直接调用toString函数
console.log(student.toString()); // '[object Object]'
// 覆盖默认的toString函数
Student.prototype.toString = function(){
return `年龄${this.age};成绩${this.score}`
}
console.log(student.toString()); // 年龄13;成绩100
默认情况下,toString()
方法被每个 Object
对象继承,如果此方法在自定义对象中未被覆盖,toString()
返回“[object type]”
,其中 type
是对象的类型。
Symbol.toStringTag
官方已经说了它定义了 Object.prototype.toString()
方法的返回值。在ES6
之后大多数内置的对象提供了它们自己的 Symbol.toStringTag
标签,toString
时回默认返回 Symbol.toStringTag
键对应的值。比如常见的如下
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
// ... and more
但是在早期不是所有对象都有 toStringTag
属性,没有 toStringTag
属性的对象也会被toString()
方法识别并返回特定的类型标签。如下:
let toStringFunc = Object.prototype.toString
toStringFunc.call('foo') // '[object String]'
toStringFunc.call([1, 2]) // '[object Array]'
toStringFunc.call(3) // '[object Number]'
toStringFunc.call(true) // '[object Boolean]'
toStringFunc.call(undefined) // '[object Undefined]'
toStringFunc.call(null) // '[object Null]'
介绍了这么多再来说我们可以 Symbol.toStringTag
做点什么?
我们自己创建的类,toString()
可就找不到 toStringTag
属性喽!只会默认返回 Object
标签
class TestClass{}
Object.prototype.toString.call(new TestClass());// '[object Object]'
我们给类增加一个 toStringTag
属性,自定义的类也就拥有了自定义的类型标签
class TestClass{
get [Symbol.toStringTag](){
return "TestToStringTag"
}
}
Object.prototype.toString.call(new TestClass());// '[object TestToStringTag]'
Symbol.asyncIterator 实现对象异步迭代器
Symbol.asyncIterator
可用于实现以一个对象的异步迭代器,多用于处理异步数据流场景
举个使用的例子: 假设开发的业务中一个功能,需要调用大量的异步请求(网络请求、数据库查询、或者文件操作),但是这个功能需要这些异步请求依次获取数据,根据 before after
结果统计出最后内容,因此需要使用异步迭代器完成。
// 异步迭代器demo
class AsyncRequest{
constructor(request){
this._request = request;
}
async *[Symbol.asyncIterator](){
for (const item of this._request) {
const res = await this._dealAsyncRequest(item);
yield res;
}
}
async _dealAsyncRequest(item){
// 模拟异步处理数据请求的过程
return new Promise((resolve)=>{
setTimeout(()=>{
resolve(item*100);
},1000)
})
}
}
(async function dealData(){
const dataSource = new AsyncRequest([1,2,3,4]);
for await(const data of dataSource){
console.log(data)
}
})()
使用 for await of
进行异步迭代时,每次迭代都会等待前一个异步操作完成,然后再进行下一次迭代,这样可确保按顺序处理每个异步操作的结果
注意:
for await of
语法是ES2018
也就是ES9
中引入的,需要在支持该语法的环境中运行
Symbol 基础类型(Reflect.Meta应用)
在定义元数据的时候 Reflect.Meta
其实它是一个全局变量,这里面的 key
很多使用 Symbol
类型,防止出现重复内容。
比如Nest框架的实现,在使用Reflect.Meta
定义 http method
元数据时,也都会使用 Symbol,
我想也是防止其他库也使用装饰器定义出相同的 key
,使用 Symbol('path')
可以避免重复问题
export const pathMetadataKey = Symbol('path');
export function GET(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
Reflect.defineMetadata(pathMetadataKey, path, target, propertyKey);
implementProcess(Method.GET, path, target, propertyKey, descriptor);
}
}
自己实现一个 Symbol
在这里仅实现一个尽量满足 Symbol
特点的函数,因为有一些能力是互相冲突的,进行了一些取舍,具体实现的能力直接在代码中进行了注释。欢迎提建议交流
(function(){
let root = this;
// 因为symbol一个特殊的能力就是可以保证对象key的唯一性
const generateName = (function(){
let postfix = 0;
return function(descStr){
postfix++;
return `&&_${descStr}_&&_${postfix}`;
}
})()
const CustomSymbol = function(desc){
// 不可以 new
if(this instanceof CustomSymbol) throw new TypeError('Symbol is not a constructor')
// desc 如果不是undefined会被toString
let descStr =desc === undefined ? undefined : String(desc);
// 需保证 symbol 值唯一性
let symbol = Object.create({
toString:function(){
return this.__Name__;
// 没有直接返回Symbol字符串是和保证作为对象key的唯一性有冲突,选择了后者 obj[symbol1] obj[symbol2]
// return 'Symbol('+this.__Desc__+')';
},
// 显示调用返回该值 隐式调用(会先调用对象的valueOf函数,如果没有返回基本值,就会再调用toString方法)
valueOf:function(){
return this;
}
});
// 保证 symbol 值唯一性
Object.defineProperties(obj,{
'__Desc__':{
value:descStr,
writable:false,
enumerable:false,
configurable:false,
},
// __Name__的generateName保证作为对象key时唯一性
'__Name__':{
value:generateName(descStr),
writable:false,
enumerable:false,
configurable:false,
}
});
return symbol;
}
let forMap = {}
Object.defineProperties(customSymbol,{
// 实现 Symbol.for
'for':{
value:function(desc){
let descStr = des
if(!Reflect.has(forMap,key)){
Reflect.set(forMap,key,customSymbol(descStr))
}
return Reflect.get(forMap,key)
},
writable:false,
enumerable:false,
configurable:false,
},
// 实现 Symbol.keyFor
'keyFor':{
value:function(symbolValue){
for (const [key,value] of forMap.entries()) {
if(value === symbolValue) return key
}
},
writable:false,
enumerable:false,
configurable:false
}
})
root.symbol = CustomSymbol;
})()
---------------------------------文章到此结束,欢迎点赞收藏-------------------------
参考文章
Symbol.toStringTag
介绍 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTagSymbol
的介绍 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol模拟实现 Symbol 类型 https://juejin.cn/post/6844903619544760328 Symbol api 真么多用处 https://mp.weixin.qq.com/s/IC9qlNrZAqNEAE8uEWpOzA Well-known Symbols https://h3manth.com/posts/Well-known-symbols/
前端请求大比拼:Fetch、Axios、Ajax、XHR 关于编码的那些事——前端应该了解的字符编码 3个容易混淆的前端框架概念 前端框架的未来:useSignal() 对于“前端状态”相关问题,如何思考比较全面