JavaScript基础: ES6新特性---Symbol

简介

在ES6中引入的一种新的基本数据类型Symbol。这个类型不像是number或者String让人使用起来那样顺手好理解,为什么是这样呢?是不是因为其是新特性,所以不熟是正常的?

自然不是因为箭头函数也是新特性为什么很多时候使用起来就顺手的多,根本原因就是直接使用的场景相对较少。

在ES6之前很多对象的属性名都是用字符串定义的变量名,这个就有一个问题,那就是后面接手的时候写新的一个对象属性的时候会将其前面进行覆盖。伪劣避免这个情况所以ES6就出现了一个数据类型Symbol类类。

Symbol对象

官网对symbol的解释:

symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。

格式:

Symbol([description])
  • description 可选

    可选的,字符串类型。对symbol的描述,可用于调试但不是访问symbol本身。简单理解可以将其看出对新建Symbol对象的的一个注释。

在这里插入图片描述

每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。

官网就说明了其唯一就是该数据仅有的目的。

来演示一下

var test=Symbol();
typeof(test); 

在这里插入图片描述

看是否是唯一值:

Symbol()==Symbol();//false
Symbol()===Symbol();//false
Symbol('test')==Symbol('test');//false
Symbol('test')===Symbol('test');//false

在这里插入图片描述

全局共享的 Symbol

上面使用Symbol() 函数的语法,不会在你的整个代码库中创建一个可用的全局的symbol类型。 要创建跨文件可用的symbol,甚至跨域(每个都有它自己的全局作用域) , 使用 Symbol.for() 方法和 Symbol.keyFor() 方法从全局的symbol注册表设置和取得symbol

前面不是说symbo表示唯一吗?但是通过Symbol.for()创建对象来看一下:

Symbol.for()==Symbol.for();//true
Symbol.for()===Symbol.for();//true
Symbol.for('test')==Symbol.for('test');//true
Symbol.for('test')===Symbol.for('test');//true

在这里插入图片描述

Symbol.keyFor(sym) 方法用来获取全局symbol 注册表中与某个 symbol 关联的键。

格式:

Symbol.keyFor(sym);
  • sym

    必选参数,需要查找键值的某个 Symbol 。

  • Symbol.keyFor(sym)返回值

    如果全局注册表中查找到该symbol,则返回该symbol的key值,返回值为字符串类型。否则返回undefined.

演示:

var globalSym1 = Symbol.for("foo");
Symbol.keyFor(globalSym1); //'foo'

var globalSym2 = Symbol.for();
Symbol.keyFor(globalSym2); //'undefined' 其实这个只是返回undefined是表示对Symbol没有写可选参数。

在这里插入图片描述

如果只是这样的话,没有多少意义了,其实Symbol.keyFor是用来判断是否以后了全局symbol对象。

如下:

var globalSym1 = Symbol.for("foo");
Symbol.keyFor(globalSym); //'foo'
var globalSym2 = Symbol.for("foo");
Symbol.keyFor(globalSym2); //undefined

Symbol和Symbol.for区别

  • 共同点: 都会生成新的Symbol
  • 不同点: Symbol.for()会被登记在全局坏境中供搜索,而Symbol()不会,Symbol.for()不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个Symbol值。

应用场景1

前面根据官方的文档的时候说过:一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的.

可以作为对象属性名的数据类型

既然这样说就有一个问题了,对象的的属性名可以用哪些数据类型作为变量名?

首先补充一个知识点:属性名中的[]作用。

属性名中的中括号有的作用就是应用变量所在代表的值作为属性名。

来演示一个:

var s="name";
var obj={
    s:"张三",
    [s]:'李四'
}

在这里插入图片描述

可以看出带上中括号直接将变量代表的值作为了属性名。

现在回归上一个话题还是老规矩这个,直接演示即可:

var s_=Symbol();
var set=new Set();
set.add(1);
set.add(2);
var obj={
    name:"法外狂徒张三",//这种是最常见的
    2:100,// 这种方式在聊伪数组的时候试过
    [s_]:'中括号引用变量值作为属性名',
    [Symbol('')]:'无描述用Symbol作为属性名',
    [Symbol('用Symbol作为属性名')]:'用Symbol作为属性名',
    [{name:"李四"}]:'对象作为属性名' ,
    [set]:"se作为属性名"
           
}

在这里插入图片描述

可以看出对象和Set不太适合作为属性值因为因为其属性名为[object object] 这样的格式就是换一个其它的也是类似显示。Symbol很多适合也相似,但是其可以通过一个描述来表示不同,不会那样令人难以理解。

而上面可以看出一个时间就是有两个属性值为[Symbol()]而且还不会彼此覆盖,所以就说明了Symbol的唯一性。

现在再来一个神奇的操作:

比如我们获得的属性的值。

如下:

在这里插入图片描述

发现这个神奇操作了,没有如果直接用属性名Symbol()等无法得到所代表的属性值,而通过s_作为属性名却可以。

这个大胆再来一个假设是不是自己的这样写方式不正确?

如下尝试:

obj[Symbol.for("b")]=123
obj[Symbol("b")]=456

在这里插入图片描述

为什么通过for就可以而通过不带for的就不行了?

本质就是如果通过for的话,其代表的Symbol值是唯一的,也就是说无论在什么地方都是这个Symbol值,而只是你不带for的话,在属性中等于又重新创建了一个新symbol所以返回一个undefined。而通过将Symbol赋值给s_然后通过s_作为属性名,则表示是一个symbol值。这也是为什么name需要带引号而s_不需要,因为它告诉浏览器这个是一个变量名,然后通过变量名得到表名所代表的值。

而如果用对象作为属性名的话也可以通过两个方式得到属性值。

var s=Symbol();
var o={test:"test"}
var obj={
    name:"法外狂狂徒",
    [s]:"symbol",
    [o]:'teessss'
}
obj["[object Object]"]// teessss
obj[o] // teessss

symbol作为属性名的特征

其实symbol作为属性名,其实还有一些神奇的地方,还是老规矩代码演示:

首先声明一个对象:

var s=Symbol();
var o={test:"test"}
var obj={
    name:"法外狂狂徒",
    [s]:"symbol",
    [o]:'teessss'
}

来一个神奇的操作:

 // 第一种 得到obj的属性名
 for(let key in  obj){
     console.log(key);
 }

在这里插入图片描述

// 第二种 获得obj的属性
console.log(Object.keys(obj));

在这里插入图片描述

所以Symbol的应用场景这个时候就看出来,作为对象属性名的使用作用:把一些不需要对外操作和访问的属性使用Symbol来定义,因为Symbol类型的key是不能通过Object.keys()或者for...in 来枚举。

当然还可以单独得到symbol私有属性,毕竟只要既然有这个属性,自然还是可以拿到的。

// 第三种  得到symbol属性
console.log(Object.getOwnPropertySymbols(obj));

在这里插入图片描述

// 第四种 获得私有属性 ES6新方式
console.log(Reflect.ownKeys(obj));

在这里插入图片描述

可以看出可以得到对象所有的属性,哪怕是Symbol定义的属性名。也就是说Symbol的属性名也是可以拿到的,但是相对其它的属性名来说需要特定的方法。

所以可以看出symbol作为属性名保证了数量属性名的唯一,同时一般也是作为不希望用户普通调用的属性。

应用场景2

可以用Symbol定义类 的私有属性或者方法。avaScript 是一弱类型语言,弱并不是指这个语言功能弱,而所指的是,它的类型没有强制性,是没有如java等面向对象语言的访问控制关键字private的,类上所有定义的属性和方法都是公开访问的,当然在TypeScript中新增了一些关键。毕竟定义属性和方法都能公开访问,都会造假一些困扰,而有了Symbol类的私有属性和方法成为了实现。

其实这个就是对场景1的一个拓展,其本质还是相似。

样子盗用他人了,大家理解一下:

let size = Symbol('size');  // 声明定义了一个size变量,类型是Symbol(),类型描述内容是size

class Collection {          // class关键字定义了一个Collection类
  constructor() {           // 构造器`constructor`函数
    this[size] = 0;         // 在当前类上私有化了一个size属性
  }

  add(item) {              // Collection类下的一个方法
    this[this[size]] = item;
    this[size]++;
  }

  static sizeOf(instance) { // 静态属性
    return instance[size];
  }
}

let x = new Collection(); // 实例化x对象
Collection.sizeOf(x) // 0

x.add('foo');       // 调用方法
Collection.sizeOf(x) // 1

Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]

这个创建了伪数组,然后让其某个一值作为一个私有属性表示伪数组的长度,而这个属性可以通过封装的方法才能得到。等于更具symbol的特性而创建的一个等同于私有属性或者方法的效果。

当然还可以结合模块也可以体现这样效果,我还没有聊模块,这个后面再聊。

应用场景3

作为一个常量的替代平,这个就引用一下React中的reducer作为公共数据状态管理的时候,如下例子:

const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE';  // 监听input框输入值的常量
const ADD_INPUT_CONTENT = 'ADD_INPUT_CONTENT';    // 添加列表
const DELETE_LIST = 'DELETE_LIST';                // 删除列表

function reducer(state, action) {
    const newState =  JSON.parse(JSON.stringify(state));
    switch(action.type) {
        case CHANGE_INPUT_VALUE:
             // ...
        case ADD_INPUT_CONTENT:
             // ...
        case DELETE_LIST;
              // ...
        default:
             return state;
    }

}

CHANGE_INPUT_VALUE三个常量如果使用字符串的,有可能会被影响,这个时候就需要一个更具保证唯一性的值,所以如下:

const CHANGE_INPUT_VALUE = Symbol('CHANGE_INPUT_VALUE');
const ADD_INPUT_CONTENT = Symbol('ADD_INPUT_CONTENT');
const DELETE_LIST = Symbol('DELETE_LIST')

应用场景4

前面聊的HTML的标签iframe的时候说过,这个标签有一个缺点那就是数据的共享也就是数据也是跨域的,这个时候如果如果需要一个共享的Symbol的类型,那么Symbol()创建的Symbol的时候只能在当前的window下使用,这个时候就需要使用Symbol.for()。

简单的说就是:Symbol.for()在全局范围内,Symbol类型值可以共享。

官方文档

属性

其实字Symbol对象中有哪些属性:

属性 描述
Symbol.asyncIterator 指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环
Symbol.prototype.description description 是一个只读属性,它会返回 Symbol对象的可选描述的字符串。
Symbol.hasInstance 用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。
Symbol.isConcatSpreadable 用于配置某对象作为Array.prototype.concat()方法的参数时是否展开其数组元素。
Symbol.iterator 为每一个对象定义了默认的迭代器。该迭代器可以被 for…of 循环使用
Symbol.match 指定了匹配的是正则表达式而不是字符串。String.prototype.match() 方法会调用此函数
Symbol.matchAll 返回一个迭代器,该迭代器根据字符串生成正则表达式的匹配项。此函数可以被 String.prototype.matchAll() 方法调用。
Symbol.replace 这个属性指定了当一个字符串替换所匹配字符串时所调用的方法。String.prototype.replace() 方法会调用此方法。
Symbol.search 指定了一个搜索方法,这个方法接受用户输入的正则表达式,返回该正则表达式在字符串中匹配到的下标,这个方法由以下的方法来调用 String.prototype.search()。
Symbol.species 知名的 Symbol.species 是个函数值属性,其被构造函数用以创建派生对象。
Symbol.split 一个正则表达式的索引处分割字符串的方法。 这个方法通过 String.prototype.split() 调用。
Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。
Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。
Symbol.unscopables 用于指定对象值,其对象自身和继承的从关联对象的 with 环境绑定中排除的属性名称。

上面这些属性,如果单独看的话,似乎有些熟悉有似乎没有属性的地方,现在举出几个聊一聊,不进行全部聊了。

Symbol.iterator

iterator这个单独如果接触过迭代器的话,应该很熟悉这个英语单词。

其实iterator是一种机制(接口):为各种不同的数据解构提供统一的访问机制,任何数据解构只要实现这个机制或者说解构,就可以完成遍历操作 【for…of】循环 ,依次处理该数据解构的所有成员。

拥有Symbol.iterator属性的数据结构都可以用for of 循环处理,而这个通过for of 循环处理数据: 数组,部分类数组(伪数组等几数据),String,Set ,Map等几种数据。

为什么这样说,老规矩看演示:

在这里插入图片描述

这个只是演示了一个String的对象,其它的就不再演示,如果有兴趣的话,可以创建数组等看一下。

而对象却不可以,因为对象没有Symbol.iterator,现在来一个演示:

在这里插入图片描述

这个时候有人就说,我可以使用【 for in 】也可以遍历属性啊。这个虽然也是遍历但是不是iterator这样机制的遍历。

演示:

var arr=['a','b','c']
for(let v of arr){
    console.log(v);
}

在这里插入图片描述

来一个对象试试:

var obj={
    name:'法外狂徒张三',
    age:1000
}
for(let v of obj){
    console.log(v);
}

在这里插入图片描述

有些说一下这个机制的特点:

  • 拥有一个next方法用于依次遍历数据结构的成员。
  • 每一个遍历返回的结果是一个对象结构:{done: false, value:xxx}
    • done: 记录是否遍历完成
    • value:当前遍历的结果。

既然知道原理了,那就自己可以搞一个了。

var obj={
    name:'法外狂徒张三',
    age:1000
}
obj[Symbol.iterator]=function(){
    let self=this,
       keys=Reflect.ownKeys(self),
       index=0;
    return{
        next(){
            if(index>keys.length-1){
                return{
                done:true,
                value:undefined
            }
               }

            return{
                done:false,
               //++ 在后意思是先运算后加
                value:self[keys[index++]]
            }
        }
    }
}


for(let v of obj){
    console.log(v);
}

这样写可以实现,但是有一个问题,如下看一下:

在这里插入图片描述

可以看出输出属性的时候这个Symbol.iterator的属性值也被输出了,这个就有新,所以这个可以在原型上操作。

所以进行以下操作

// 第一步将其Symbol.iterator 删除
delete obj[Symbol.iterator];

// 第二步在原型上操作, 当然这个也就赋予了所有的额对象一个[Symbol.iterator];
Object.prototype[Symbol.iterator]=function(){
    let self=this,
       keys=Reflect.ownKeys(self),
       index=0;
    return{
        next(){
            if(index>keys.length-1){
                return{
                done:true,
                value:undefined
            }
               }

            return{
                done:false,
               //++ 在后意思是先运算后加
                value:self[keys[index++]]
            }
        }
    }
}

在这里插入图片描述

既然知道原理了,设置可以i修改返回的效果。

// 第一步将其Symbol.iterator 删除
delete obj[Symbol.iterator];

// 第二步在原型上操作, 当然这个也就赋予了所有的额对象一个[Symbol.iterator];
Object.prototype[Symbol.iterator]=function(){
    let self=this,
       keys=Reflect.ownKeys(self),
       index=0;
    return{
        next(){
            if(index>keys.length-1){
                return{
                done:true,
                value:undefined
            }
               }
           let key=keys[index++];
            return{
                done:false,
               //++ 在后意思是先运算后加
                value:{
                    key,
                    value:self[key]
                }
            }
        }
    }
}

在这里插入图片描述

Symbol.hasInstance

Symbol.hasInstance用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。

还是老规矩来例子演示:

class Test{
    constructor(){
        this.flag=Symbol.for('flag');
    }
}
var test=new Test();
console.log(test instanceof Test);

在这里插入图片描述

而这个很正常,毕竟是通过Test而new的一个对象,自然返回一个值true。

这个就需聊要ES6的一个属性:Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null.而目前先记住这个Object上的方法(擦 感觉在不停的挖坑,以后慢慢补上)。如果记不住的的,看演示:

var arr=[1,2,3];
console.log(arr instanceof Test);

在这里插入图片描述

可以看出返回的是false。而现在看一下Object.setPrototypeOf()神奇操作。

//  这里用arr.__proto__=new Test; 不过是因为有些浏览器不兼容所以通过Object.setPrototypeof
Object.setPrototypeOf(arr, Test.prototype);
console.log(arr instanceof Test);

在这里插入图片描述

可以看出其实重构原型链会影响instanceof的返回值,而比如有些只有在new的对象通过instanceof才可以返回true,那如何办?

这个就有一个问题了,既然在聊Symbol.hasInstance的时候说instanceof ,那自然就有关系了:

//看见 obj instanceof  objC

//第一步: 先找objC[Symbol.hasInstance] 如果有会根据这个返回的结果

// 第二: 如果没有再执行再原型链上查找 是否种子这个构造函数

老规矩进行演示:

class Test{
    constructor(){
        this.flag=Symbol.for('flag');
    }
    static[Symbol.hasInstance](obj){
        // 为什么这样写 如果不太懂可以看一下前面的文章
         return obj.flag&&obj.flag===Symbol.for('flag');
    }
}
var arr=[1,2,3];
Object.setPrototypeOf(arr, Test.prototype);
console.log(arr instanceof Test);

在这里插入图片描述

Symbol.toPrimitive

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。

还是大白话说就是:对象转化为原始值类型(如:数字或者字符串)的时候,先执行[Symbol.toPrimitive]这个方式。

// 比如 0=={}

// 第一步:首先看对象是否有Symbol.toPrimitive这个属性方法,如果有这个属性方法就按照这个处理。

// 第二步 如果没有这个属性方法,就调用valueOf() 验证是否是有原始的值属性。

// 第三步: 如果没有原始值,那么对象就调用toString() 转换为字符串

// 第四步:  再根据字符串看是否可以转换为数字


格式如下:

const object1 = {
  [Symbol.toPrimitive](hint) {
   
  }
};

在 Symbol.toPrimitive 属性(用作函数值)的帮助下,一个对象可被转换为原始值。该函数被调用时,会被传递一个字符串参数 hint ,表示要转换到的原始值的预期类型。 hint 参数的取值是 "number"、"string" 和 "default" 中的任意一个。

老规矩实例演示:

// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果
var obj1 = {};
console.log(+obj1);     // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"

// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if (hint == "number") {
      return 10;
    }
    if (hint == "string") {
      return "hello";
    }
    return true;
  }
};
console.log(+obj2);     // 10      -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); // "true"  -- hint 参数值是 "default"

例子来自官网,现在来一个面试题判断结果:

var obj={
    num:0
};
obj[Symbol.toPrimitive]=function(){
    return ++this.num;
}

if(obj==1&& obj==2&obj==3){
    console.log('宫崎老贼牛逼');
}

大家觉得会有结果吗?先不要看下面,先向下然后再看:

在这里插入图片描述

解析这道题涉及的知识点:

  • 第一个知识点 : 那就是== 的隐式转换 这个可以在我前面文章看隐式转换
  • 第二个知识点: 那就是 ++ 在数字的前后有什么区别,在前是先加在后是后加。
  • 第三个知识点:[Symbol.toPrimitive] 这个在对象的静态方法尤其在转换数字的时候先调用这个方法,这个方法有返回值了,那么后面也就没必要在找了。

滤清了三个知识点,估计这个题也就明白了,如果不懂私聊我,可以单独讲解。

Symbol.toStringTag

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。

在聊着这个时候,先搞一个传统项目扯看似无关的而又有关的例子。

首先聊一个得到数据类型的方式,这个前面也聊过:判断数据类型的三种方式

许多内置的 JavaScript 对象类型即便没有 toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签,比如:

Object.prototype.toString.call('foo');     // "[object String]"
Object.prototype.toString.call([1, 2]);    // "[object Array]"
Object.prototype.toString.call(3); 

举一个例子看:

在这里插入图片描述

另外一些对象类型则不然,toString() 方法能识别它们是因为引擎为它们设置好了 toStringTag 标签:

Object.prototype.toString.call(new Map());       // "[object Map]"
Object.prototype.toString.call(function* () {
    
    }); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"

举一个例子:

在这里插入图片描述

看到了,这个时候说这个有什么意义呢?

如演示:

class Test{
}
var test=new Test();
Object.prototype.toString.call(test);

在这里插入图片描述

比如我要求必须输出类型为Test如何?

class Test{
    //因为是属性而不是方法,需要需要get方法
    get [Symbol.toStringTag](){
        return 'Test';
    }
}
var test=new Test();
Object.prototype.toString.call(test);

 

在这里插入图片描述

其它的属性就不再多余演示了,其实都是类似,按其属性就明白了这个和相关的对象方法都是有关的,准确的说是影响了一些方法的机制。如果想了解更多可以看下官网,也可以私信我。

猜你喜欢

转载自blog.csdn.net/u011863822/article/details/124757681