查看原文
其他

作为前端的你竟然还没用过Symbol!

脚本之家 2023-12-27

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 类型数据遍历出来的函数

  1. Object.getOwnPropertySymbols 方法可以获取指定对象的所有 Symbol 属性名
  2. 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) ]
  1. 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([12])                       // '[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;
})()

---------------------------------文章到此结束,欢迎点赞收藏-------------------------

参考文章

  1. Symbol.toStringTag 介绍 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
  2. Symbol的介绍 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol
  3. 模拟实现 Symbol 类型 https://juejin.cn/post/6844903619544760328
  4. Symbol api 真么多用处 https://mp.weixin.qq.com/s/IC9qlNrZAqNEAE8uEWpOzA
  5. Well-known Symbols https://h3manth.com/posts/Well-known-symbols/
  推荐阅读:
  1. 前端请求大比拼:Fetch、Axios、Ajax、XHR
  2. 关于编码的那些事——前端应该了解的字符编码
  3. 3个容易混淆的前端框架概念
  4. 前端框架的未来:useSignal()
  5. 对于“前端状态”相关问题,如何思考比较全面
继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存