ES6的Symbol:设计师拼命藏起来的数据类型

651 阅读5分钟

为什么要有Symbol?

Symbol 是ES6引入的一种新的原始数据类型,旨在为对象提供唯一的标识符。返回的是唯一值。

假设这么一个场景:如果在一个大型项目中,使用的对象十分复杂,这个项目是多人协作完成的,也可能是已经离职的程序员写的。你并不知道哪些变量或常量被使用过了,如果设计重复,可能会导致整个项目不能够运行。在这时就需要我们的Symbol了。

Symbol,它用于创建唯一的键。这为解决对象属性命名冲突的问题提供了可能,并且使得对象的设计和实现更加灵活。每个创建的 Symbol 都是独一无二的,即使它们拥有相同的名字。这一特性使得 Symbol 成为了一个理想的工具,用于防止属性名冲突,并且可以在多个开发者或库之间共享代码时保持对象属性的安全性和独立性,能够有效避免命名空间污染问题,

56556.jpg

创建与使用Symbol

虽然是原始数据类型,但是Symbol在创建的时候却是用函数的方法。Symbol函数用于声明一个新的Symbol实例。每个Symbol实例都是独一无二的,即使标签相同,也不会影响其独特性。

    // 为什么要有symbol呢? 放心把他加进去,绝对不会重复
   const wes = Symbol('Wes');
   const classMate={
        // 字符串 覆盖
        "cy": 1,
        "cy": 2,
        // Symbol 可以代替 字符串作为key 
        [Symbol('Mark')]: {grade: 50,gender: 'male'},
        [Symbol('olivia')]: {grade: 80,gender: 'female'},
        [Symbol('olivia')]: {grade: 80,gender: 'female'}
   }
   console.log(classMate);

image.png 可以看到,在上面的例子中,我们在classMate中用Symbol创建了两个olivia,并没有发生错误,这就是Symbol

对象字面量中的 [ ]

我使用了[ ]来包裹Symbol,如果不使用[ ],就会报错。这是为什么呢?

别着急,我们先来了解在哪些情况下需要使用[ ],再一一讲解。

在JS中,当你在对象字面量中使用方括号[]来定义键时,通常是为了动态地创建属性名或者使用那些不能直接用点符号(.)或直接作为键名的特殊字符、保留字或变量

1. 动态键名

如果你想要根据运行时的值来决定对象的键名,那么你就需要用方括号语法。

let name = 'zhangsan'
const classMate={
     [name]: "猛男",
}
console.log(classMate[name]);    //  猛男
console.log(classMate.zhangsan);    //  猛男

2. 使用表达式作为键名

方括号允许你使用表达式作为键名,这可以包括函数调用、算术运算等。

let prefix = 'zhang'
const classMate = {
    [prefix + 'san']: "猛男",
}
console.log(classMate.zhangsan);  // 猛男

3. 使用保留字或包含特殊字符的键名

某些词是JS的保留字,如classfunction等,如果尝试直接使用这些词作为对象的键名会导致语法错误。另外,如果键名包含空格或特殊字符,你也必须使用方括号。

let obj = {
    ['class']: "数学",
    ['nam e']: "zhangsan"      
};
console.log(obj['class']);    // 数学         
console.log(obj['nam e']);    // zhangsan

4. 使用 Symbol 类型作为键

由于 Symbol 是一个函数,并且返回的是一个唯一值,所以不能直接用点符号访问。因此,使用方括号是唯一的方法

let name = Symbol('Mark')
const classMate={
     // [Symbol('Mark')]: {grade: 50,gender: 'male'},
     // 拆开来看就很好理解了
     [name]: {grade: 50,gender: 'male'},
}
console.log(classMate[name])     // { grade: 50, gender: 'male' }

Symbol 与 遍历

前面都很顺利,但是当我们要遍历Symbol的时候出现了一点小问题,Object.keys()Object.values()以及Object.entries()等方法,用于遍历对象的属性,但这些方法都不会包含Symbol类型的键名或键值。设计师认为它很重要,不能随便让人看,把它藏起来了。

Object.keys()Object.values() 以及 Object.entries() 都是Object上的静态方法,并不是定义在 Object.prototype 上,只能通过Object获取到。

const classMate={
     "cy": 2,
     [Symbol('Mark')]: {grade: 50,gender: 'male'},
     [Symbol('olivia')]: {grade: 80,gender: 'female'},
     [Symbol('olivia')]: {grade: 80,gender: 'female'}
 }
 for(let [key,val] of Object.entries(classMate)){
     console.log(key,val);  // cy 2
     // 我们不能遍历到 Symbol ,就好像它被藏起来了一样
 }

属性描述符

属性描述符(Property Descriptors)用于定义对象属性的特性。每个属性都有一个对应的描述符,它指定了该属性的各种元数据信息。属性描述符有两种主要类型:数据描述符存取描述符。根据本文的需求,我们先来介绍数据描述符

  • value:属性的值,默认为undefined
  • writable:是否可写
  • enumerable:是否可枚举(能够循环到)
  • configurable:是否可以修改其他描述符

我们来查看一下classMate的属性描述符,使用一个名字贼拉长的方法getOwnPropertyDescriptors

//  打印对象的所有 属性描述符
console.log(Object.getOwnPropertyDescriptors(classMate))

image.png

好像更迷惑了。诶?这Symbolenumerabletrue啊,为什么会遍历不到呢?

由此,我们来引出Symbol的特性

Symbol 特性 + 访问方式

Symbol 的设计初衷就是为了提供一种不会被意外遍历到的属性键,以避免与对象上其他属性发生冲突:

  • 唯一性:每个 Symbol 实例都是唯一的,这使得它们非常适合用作对象属性键,尤其是在需要确保键名不冲突的情况下。
  • 隐私性:尽管你可以通过特定的方法访问 Symbol 键,但默认情况下它们不会出现在大多数遍历操作中。这种特性可以用来创建“私有”属性,这些属性不容易被外部代码无意中访问或修改。

都到这里了,如果你实在是想要获取对象上的所有Symbol类型的键,可以使用一个名字更长的方法:Object.getOwnPropertySymbols。这个方法会返回一个数组,其中包含了指定对象自身所有的Symbol属性。

// 上面的例子
const syms=Object.getOwnPropertySymbols(classMate)
console.log(syms);
    
const data=syms.map(sym=>classMate[sym].grade)
console.log(data);

藏了半天,我们终于还是拿到了Symbol !!!

image.png

总结

综上所述,Symbol类型为JS开发者提供了一个强大的工具,用于创建独特的键名,从而避免了命名冲突的问题,同时增强了代码的健壮性和可维护性。在处理复杂对象结构或需要保证键名唯一性的场景下,合理利用Symbol可以显著提升开发效率和代码质量。