Symbol已经出来8年多了,你真的了解过它吗

前言

2015年6月ES6正式发布,距今已经有八年多快九年的时间了。

众所周知,在ES6发布的时候伴随着新出了一个新的数据类型Symbol。我们知道Symbol代表的值是独一无二的,任何情况下两个Symbol类型的值都是不同,Symbol()!==Symbol()。因此Symbol类型的值非常适合作为封装的对象里的key来使用,因为这样即使别人使用你封装的这个对象,再添加新的key到这个封装对象里,无论如何也不会覆盖原有的key。除此之外非要查找对象里以Symbol作为key的可以使用Object.getOwnPropertySymbols(obj)或者使用Reflect.ownKeys(obj)来查找Symbol类型的key

好了今天的文章就到此结束了 (。・ω・。)。

当然Symbol的用途不止这些,这些只是它的基本使用,那还有其他用的用途是什么呢?

正文

Symbol本身有着数个属性和方法,方法也许不是很重要,但是那些属性才是除了Symbol的特点之外核心所在。这里Symbol属性基本是作为动态的方法或属性名来使用。

Symbol属性

Symbol.iterator

iterator译为迭代器。迭代器是什么,它允许你遍历一个集合的元素(例如数组、字符串、Map、Set等)而不需要知道集合的底层表示方式。迭代器对象提供了一种方法来访问集合的下一个元素,直到所有元素都被访问完。也许你没有在意过,但是能被遍历集合的原型上都存在Symbol.iterator属性。

在ES6中同样新引入了一个遍历方法for..of,当使用for..of对元素进行遍历的时候,会先看或者元素是否是迭代器元素(也可以理解为这个元素上是否存在Symbol.iterator ),所以当使用for..of遍历Object类型的元素时,会报错,因为Object类型的元素上不存在Symbol.iterator

js 复制代码
 const obj = {
      name: "iceCode",
      age: 18,
      site: "成都",
    };
    for (let key of obj) {
      console.log(key);
    }

这也可以引出另外一个问题,如何使用for..of遍历Object的元素?

js 复制代码
//这里仅需简单的改造一下
 const obj = {
      name: "iceCode",
      age: 18,
      site: "成都",
      //当使用for..of遍历元素的时候,会自动调用Symbol.iterator这个方法,如果没有则报错
      [Symbol.iterator]: function* () {
        for (let key of Object.keys(this)) {
          yield key;
        }
      },
    };
    for (let key of obj) {
      console.log(`使用for..of遍历出的${key}`);
    }
生成器函数

这里Symbol.iterator上的方法是生成器函数,简单的介绍一下生成器函数,使用function定义函数时在function 和函数名之间加个*号 ,函数内要返回值不再是return关键字,而是使用yield关键字返回值。可以放回多个值,访问时则是使用 fn().next().value来访问,每次仅可访问一个数据,可以使用for..of批量访问生成器函数内的数据。

仅需知道生成器函数最初是为了解决在处理大量数据时,一次性将所有数据加载到内存中可能会导致内存溢出和回调地狱而的问题。但随后有了Promise、async/await之后,此函数便几乎不再使用。

js 复制代码
function* fn() {
      const a1 = yield 111;
      console.log(a1); // 222
      //这里的a1简单理解是下面调用next()方法传入的参数
      const a2 = yield a1 + 1;
      //这里a2同理
      console.log(a2); // 333
      const a3 = yield a2 + 1;
      //a3为undefined是应为没有第四次调用next()方法,也不存在传入有的参数
      console.log(a3); // undefined
    }
    const a1 = fn();
    //这里所有的值都是yield返回的值,而不是传入的值
    console.log(a1.next().value); //111
    console.log(a1.next(222).value); //223
    console.log(a1.next(333).value); //334
    
    // 使用for...of循环 因为直接可以显示出来,并没有next()参数,这里从第二个开始就是undefined+1 自然是NaN
    for (const i of a1) {
      console.log(i); //111 NaN NaN
    }

回到正题,除了在使用for...of时会自动调用Symbol.iterator的方法,还有一种情况下也会自动调用Symbol.iterator方法,就是ES6中的解构赋值,当重新解构为一个数组时,就会自动调用Symbol.iterator方法

这里又可以引出另外一个问题,怎么才能让这个公式成立 const [a,b]={a:1,b:2}(可能平时没人会傻到这样解构赋值),这种情况就可以根据以上的方法举一反三来解决。

js 复制代码
 const obj = {
      a: 1,
      b: 2,
      [Symbol.iterator]: function* () {
        yield this.a;
        yield this.b;
      },
    };
    //这里解构赋值是按照Symbol.iterator方法返回的迭代器来解构的  改变yield的顺序就可以改变解构的顺序
    const [a, b] = obj; // 输出:1 2

另外当使用扩展运算符时,也会自动调用Symbol.iterator,假如是Object元素,只有在{...obj}的情况下不会报错(自动调用Symbol.iterator),既然会自动调用Symbol.iterator,那就添加上即可

Symbol.asyncIterator

asyncIterator异步迭代器,顾名思义,这个同迭代器基本类似,但区别不同是的这个主要处理异步方法。在for await ...of时会自动调用。

js 复制代码
//此方法同iterator类似就不多做介绍了,主要讲一些独占的差别
const p = (v) =>
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(v);
        }, v);
      });
    const obj = {
    //for...of 只会调用这个
      *[Symbol.iterator]() {
        yield p(222);
        yield p(111);
        yield p(333);
      },
      //for await ...of 只会调用这个
      async *[Symbol.asyncIterator]() {
        yield p(2222);
        yield p(1111);
        yield p(3333);
      },
    };
    
    //最终调用的顺序也是从上到下,但是两个调用的方法不同
    async function an() {
      for await (const item of obj) {
        console.log(item);
      }
      for (const item of obj) {
        const i = await item;
        console.log(i);
      }
    }
    an();

Symbol.hasInstance

简单来说,此属性会在使用 instanceof 时自动调用。传入一个参数,为instanceof前的元素。

官方来说,instanceof 运算符 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,Symbol.hasInstance 用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。

js 复制代码
 class Obj {
      //在class中的这个方法必须加 static 关键字,否则无法被instanceof访问到
      static [ Symbol.hasInstance ]( instance ) {
        //这里接收的参数就是instanceof运算符右边的实例
        console.log(instance);//[]
        return instance;
      }
  }
    //对象中同上,但一般instanceof基本是用在构造函数上
    const obj = {
      [Symbol.hasInstance](instance) {
        console.log(instance);
        return instance;
      },
  };
    //因为Symbol.hasInstance方法中的返回值问题,无论什么时候都是true
    console.log([] instanceof obj); // 输出 true
    console.log([] instanceof Obj); // 输出 true

Symbol.isConcatSpreadable

该属性用于在数组类型元素使用.concat()合并时是否展开数组元素内的数据

js 复制代码
//此属性很好理解,看例子瞬间秒懂,就是数组类型元素将Symbol.isConcatSpreadable的值设置为false时
//将不会展开数组内的元素进行合并,将直接把第数组push到元素内
    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];
    const arr3 = arr1.concat(arr2); // [1, 2, 3, 4, 5, 6]
    arr2[Symbol.isConcatSpreadable] = false; // 禁止展开数组元素,即不使用concat方法合并数组元素。
    const arr4 = arr1.concat(arr2); // [1, 2, 3, [4, 5, 6]]
    console.log(arr3, arr4);

Symbol.search

在作为字符串方法search的参数时,会自动调用Symbol.search方法,传入一个参数,当前调用search方法的字符串。字面意思可能不太很好理解,看例子。

js 复制代码
//定义一个简单的对象
const obj = {
      name: "iceCode",
      site: "chegndu",
      hobby: "code、music、game",
      //obj作为search的参数时,此方法会被自动调用
      [Symbol.search](str) {
      //这里 str==='code' 使用toLowerCase是为了不区分大小写
        str = str.toLowerCase();
        const arr = [];
        for (const key in this) {
        //如果此对象中哪个属性值包含'code',则会将key添加到数组中并返回
          this[key].toLowerCase().includes(str) && arr.push(key);
        }
        return arr;
      },
    };
    //所以在这里得到的结果为 属性值里包含'code'的key的数组
    console.log("code".search(obj));//['name', 'hobby']
    
    //当然也可以是class类
    class Cla {
      constructor(value) {
        this.value = value;
      }
      //不论是对象还是类,只要是作为search的参数时,这个方法就会被调用
      [Symbol.search](str) {
      //这里str依旧是调用search的字符串 不区分大小写返回索引
        return str.toLowerCase().indexOf(this.value.toString().toLowerCase());
      }
    }
    //所以这里得到的结果为 1
    console.log("name".search(new Cla("a")));//1

Symbol.match

此属性用于设置是否为正则表达,通常用在正则表达上。

js 复制代码
//如果字符串使用startsWith方法来判断开头的字符串是否为参数内的元素时,传入一个正常的正则表达则会报错
'/name/'.startsWith(/name/)
js 复制代码
const exp = /de/;
//改变正则的Symbol.match方法为false时,此正则表达式不在作为正则表达式,而是字符串
exp[Symbol.match] = false;
//虽然这里类型判断依旧是正则类型
console.log(Object.prototype.toString.call(exp)); //[object RegExp]
//这里就可以正常判断了
console.log("/de/".startsWith(exp));//true

另外它也可以是一个函数,既然是Symbol.match了,那么也肯定会在字符串的match方法的参数时自动被调用

js 复制代码
const exp = /de/;
console.log("/desdfasdf/".match(exp));

正常情况下返回一个数组,里面是对匹配的一些详细描述,那么自动调用可以稍微改造一下

js 复制代码
 const exp = /de/;
     //在作为match参数时自动调用
    exp[Symbol.match] = function (v) {
        //参数v就是字符出,this为正则表达式本身
      console.log(v, this);
      return {
        isMatch: this.test(v), // 返回一个布尔值,表示是否匹配成功
        index: v.indexOf(this.toString().replaceAll("/", "")),//判断索引位置
        describe: `匹配${this.test(v) ? "成功" : "不成功"},匹配的位置在索引${v.indexOf(this.toString().replaceAll("/", ""))}`,
      };
    };
    console.log("/desdfasdf/".match(exp));

Symbol.matchAll

此属性作为正则表达式的方法名,可接受一个字符串参数,返回一个该正则表达式匹配的字符串参数的迭代器。

介绍看着有点抽象,直接上代码

js 复制代码
    //创建一个只匹配数字的正则表达式
    const exp = /[0-9]+/g;
    //这里是要匹配的字符串
    const str = "2023-12-31|2024-01-01";
    //当我们调用它的Symbol.matchAll方法并传入字符串时,则会返回一个迭代器,该迭代器可以遍历所有匹配结果
    const result = exp[Symbol.matchAll](str);
    console.log(result); //直接打印这个结果只能看到正则表达式字符串迭代器,什么内容都无法看到
    //前面说了迭代器,那么既然是迭代器,我们就可以用for...of循环来遍历它
    for (const match of result) {
      console.log(match);
    }

可以看到最终的结果,是match方法的一个集合

js 复制代码
//如果是需要匹配到的结果,可以是用Array.form()方法,同样是找到迭代器然后返回一个浅拷贝的数组
console.log(Array.from(result, (x) => x[0]));//['2023', '12', '31', '2024', '01', '01']

同样,如果是作为字符串的matchAll的参数时,这个方法也会被自动调用,如果需要也可以同match一样将此方法重写,因为相同这里就不做演示了。

Symbol.species

官方描述:知名的 Symbol.species 是个函数值属性,其被构造函数用以创建派生对象。另外也可以简单的理解为在调用方法重新指向时会自动调用这方法,并可以在此方法内重新指向。

js 复制代码
//这里继承一下Array,创建一个自己的类
class MyArr extends Array {
//这里必须要有static get 两个关键字,少一不可
      static get [Symbol.species]() {
      //在调用Array方法,重新返回当前Array时会自动调用。这种方式可以使Array类型的数据进行链式调用
        console.log("gaibian");
        //这里不再返回原有的MyArr类,直接返回Array
        return Array;
      }
    }
    //创建arr实例
    const arr = new MyArr(1, 2, 3, 4);
    //当使用arr方法需要返回一个数组
    const mapArr = arr.map((item) => item);
    //可以发现 当使用instanceof时,他的构造类不再是MyArr,而是Array
    console.log(mapArr instanceof MyArr); // false
    console.log(mapArr instanceof Array); // true

Symbol.replace

根据前面的规律,顾名思义,此属性方法将会元素作为字符串的replace方法的参数时调用,传入一个参数,为当前调用repalce的字符串。

js 复制代码
 const obj = {
      name: "icecode",
      age: 18,
      //当obj作为replace的参数时,此方法会自动调用,返回的结果则是replace之后的结果
      [Symbol.replace](str) {
      //这里随便写个判断 在obj中第一个匹配到str的则会返回这个值
        for (const key in this) {
          if (typeof this[key] === "string" && this[key].includes(str)) {
            return this[key];
          }
        }
      },
    };
    //这里的结果就是匹配到的值
    console.log("code".replace(obj));//icecode
  </script>

Symbol.split

同上,当作为split的参数时,此属性方法将会自动被调用,传入一个参数,为当前调用split方法的字符串

js 复制代码
//这里应该很容易就能看懂,obj作为参数时,调用Symbol.split方法,返回了一'-'拼接的字符串
    const obj = {
      [Symbol.split](str) {
        let strs = "";
        for (const s of str) {
          strs += s + "-";
        }
        return strs.substring(0, strs.length - 1);
      },
    };
    console.log("code".split(obj));//c-o-d-e

Symbol.toPrimitive

当进行类型转换时则会自动调用此方法,方法传入一个参数,参数类型只有三种'number'、'string'、'default'。方法的返回值

  1. 当使用算数运算符时,类型为number
  2. 在模板字符串中时,类型为string
  3. 其余类型转换情况下,类型为default
js 复制代码
//对象或者类都行
 const obj = {
      [Symbol.toPrimitive](hint) {
        console.log(hint);
        return 1;
      },
    };
    //这中算数运算符或者强制性数字转换的情况下,hint的值为number
    console.log(obj * 1);
    console.log(obj / 1);
    console.log(obj - 0);
    console.log(+obj);
    console.log(Number(obj));
    //当在模板字符串中或者强制性字符串转换,hint的值为string
    console.log(`${obj}`);
    console.log(String(obj));
    //只有在这种情况,hint的值才为default
    console.log(obj + "");
    //这种情况下是不会触发Symbol.toPrimitive方法的
    console.log(Array(obj));
    console.log(Object(obj));

另外此属性还可以作为Symbol类型实例的方法调用,将Symbol类型转换为原始值

js 复制代码
const name = Symbol("iceCode");
//这里的结果是全等的
console.log(name === name[Symbol.toPrimitive]());//true

Symbol.toStringTag

此方法用于创建自定义字符串描述,在js类型判断中,我们知道有多种类型判断方法,比如typeof、instanceof、constructor等来判断,但是这些判断方法都不是很全面。有一个判断方法则是非常全面Object.prototype.toString.call()方法,返回一个json类型的类数组数据,第二个参数则是该元素的类型。

Symbol.toStringTag方法则是在作为Object.prototype.toString.call()的参数时会被调用,返回的值就是这个类数组的第二个参数。也就是说Object.prototype.toString.call()的类型判断非常全面,但是类型是可以被改变的。

js 复制代码
     class Obj {
     //当Obj类作为Object.prototype.toString.call参数时,此方法就会被调用,get 是必须要有的
      get [Symbol.toStringTag]() {
        return "string";
      }
    }
    //正常情况下这里返回的[object Object],但是这里是[object string],改变了它本应该的类型
    console.log(Object.prototype.toString.call(new Obj()));//[object string]
    
    //继承了Promise后,返回一个number
    class MyP extends Promise {
      get [Symbol.toStringTag]() {
        return "number";
      }
    }
    //这里只是单独继承一下Promise
    class P extends Promise {}
    
    //可以看出两者的类型并不相同
    console.log(Object.prototype.toString.call(MyP.resolve()));//[object number]
    console.log(Object.prototype.toString.call(P.resolve()));//[object Promise]

另外,在我们判断DOM类型的时候,也可使用Symbol.toStringTag快捷显示类型。这种直接读取类型的方式仅只能在DOM上生效。

js 复制代码
//获取DOM
const div = document.querySelector("div"); 
//当我们想要知道这个DOM具体是什么类型,可以间的使用DOM.toString(),则返回json类型的类数组数据
console.log(div.toString());//[object HTMLDivElement]
//我们也可以使用Symbol.toStringTag,这里直接读取即可,直接返回DOM具体类型
console.log(div[Symbol.toStringTag]);//HTMLDivElement

Symbol.unscopables

Symbol.unscopables用于指定一个对象,该对象的属性名称不会包含在环境绑定中。换句话说,当使用with语句时,这些属性名称将从with环境绑定中排除。

这个属性通常用于定义在with语句中不应被搜索的属性。通过在对象的Symbol.unscopables属性上设置属性为true,可以使该属性变得不可作用域(即不会在词法环境变量中出现)。这在某些情况下可以防止属性被意外覆盖或修改。

需要注意的是,Symbol.unscopables属性在非严格模式下可以使用with语句。在严格模式下,with语句将不可用,因此Symbol.unscopables属性也不会有任何效果。

此外,Symbol.unscopables属性通常用于自定义对象的行为,以便在with语句中更好地控制属性的访问和修改。这对于编写库或框架的人来说可能是有用的,但对于大多数日常编程任务来说,可能并不需要直接使用这个属性。

js 复制代码
const object1 = {
      property1: 1,
      property2: 2,
    };

    object1[Symbol.unscopables] = {
      //这里设置为true的时候,with语句就不会访问object1.property1了,因为unscopables属性被设置为true了。
      property1: true,
      property2: false,
    };

    with (object1) {
      console.log(property2);
      //这里则会报错,因为with语句不会访问object1.property1
      console.log(property1);
    }

description

此属性为原型上的属性,它会返回 Symbol 对象的可选描述的字符串。

js 复制代码
     const name = Symbol("iceCode");
     const age = Symbol(18);
     //这里返回的是Symbol描述,描述的类型全为string类型,不会按原类型返回
     console.log(name.description);//iceCode
     console.log(age.description);//18

Symbol方法

Symbol.for()

我们知道,Symbol类型的值永远都不会相等,即使Symbol的参数一样。Symbol.for()方法就是为了解决你想使用Symbol类型数据,但在相同参数的情况下需要相等的问题。

Symbol.for(key) 方法会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。

js 复制代码
    //虽然Symbol参数相同,但他们不相等
    const name = Symbol("iceCode");
    const name1 = Symbol("iceCode");
    console.log(name === name1); // false
    //创建一个共享的symbol,如果已经存在,则返回该symbol,否则创建一个新的symbol。
    const forName = Symbol.for("iceCode");
    const forName1 = Symbol.for("iceCode");
    //使用for方法后,如果是相等参数,则它们是同一个Symbol
    console.log(forName === forName1); // true
    //他们类型是相同的
    console.log(typeof forName === typeof name);//true

Symbol.keyFor()

此方法是为了寻找在symbol注册表中关联的键。使用for方法关联的键使用keyFor则会被返回。

js 复制代码
//创建一个共享的symbol,如果已经存在,则返回该symbol,否则创建一个新的symbol。
const forName = Symbol.for("iceCode"); 
//很简单理解 ,就是为了查找for注册的键
console.log(Symbol.keyFor(forName));//iceCode

toString()

toString() 方法返回当前 symbol 对象的字符串表示。

js 复制代码
const forName = Symbol("iceCode"); 
//这里如果直接使用Symbol类型的数据加上字符串则会报错
console.log(forName + "to");
js 复制代码
 const forName = Symbol("iceCode");
 //toString之后可以正常拼接字符串
 console.log(forName.toString() + "to");//Symbol(iceCode)to

valueOf()

valueOf() 方法返回当前 symbol 对象所包含的 symbol 原始值。其他类型一般情况下会进行隐式调用,但是Symbol有些特殊,它不会隐式调用valueOf。所以这里valueOf方法几乎没有任何作用。

结尾

Symbol类型从出来之后一般业务就很少能够使用到,即使最基本的Symbol(key)也很少有作为对象键使用的。但对于很多工具库来说,Symbol类型的出现可能解决了很多不必要的麻烦和减轻了一些被用户使用库时命名冲突问题。

其中的些方法和属性对于我们写业务的人来说,其实并不是很重要,这里以上的内容可以作为了解看一下,可以让你对Symbol有更多的了解(有可能迭代器iterator会在面试时问到),谢谢。

相关推荐
-seventy-4 分钟前
对 JavaScript 原型的理解
javascript·原型
&白帝&22 分钟前
uniapp中使用picker-view选择时间
前端·uni-app
谢尔登29 分钟前
Babel
前端·react.js·node.js
ling1s29 分钟前
C#基础(13)结构体
前端·c#
卸任35 分钟前
使用高阶组件封装路由拦截逻辑
前端·react.js
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
秋沐1 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
这个需求建议不做1 小时前
vue3打包配置 vite、router、nginx配置
前端·nginx·vue
QGC二次开发1 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
云草桑1 小时前
逆向工程 反编译 C# net core
前端·c#·反编译·逆向工程