关于 JavaScript 中的 Symbol 类型和它的应用

251 阅读6分钟

Symbol 介绍

JavaScript 的 Symbol 是 ES6 引入的一种新的原始数据类型,它提供了一种方式来生成一个全局唯一的标识符。这个特性主要用于作为对象属性的键,这些键不会与其他属性键冲突,即使它们有相同的名称也是如此。

为什么需要 Symbol?

想象一下,在一个大型项目中,多个开发团队同时工作于同一个代码库上。他们各自添加功能和属性到对象中,但有时可能会不小心使用了相同的属性名。这会导致属性值被意外覆盖,进而引发难以追踪的错误。为了防止这种情况的发生,JavaScript 引入了 Symbol 类型。每个通过 Symbol() 创建的实例都是独一无二的,即使它们有相同的标签(label),这也确保了不同的开发者可以安全地向对象添加新的属性而不会发生冲突。

Symbol 的特性

  • 唯一性:每个 Symbol 都是唯一的,即使是用相同的标签创建的两个 Symbol 也是不同的。
  • 可选标签:你可以给 Symbol 提供一个描述性的标签,但这仅仅是为了方便调试,并不影响 Symbol 的唯一性。
  • 作为对象键:由于 Symbol 的唯一性,它非常适合用来作为对象的属性键,从而避免与其他属性发生命名冲突。
  • 隐私保护:当 Symbols 作为对象属性时,它们不会出现在 Object.keys()Object.values() 和 Object.entries() 的结果中,因此提供了某种程度上的“私密性”。只有通过 Object.getOwnPropertySymbols() 方法才能访问这些属性。

Symbol 的基本使用

要创建一个Symbol,你只需调用 Symbol() 函数。你可以选择性地给 Symbol 传递一个描述字符串,该字符串在调试时很有用,但不影响 Symbol 的唯一性:

const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2); // 输出:false

即便两个 Symbol 的描述相同,它们也是完全不同的值。

Symbol 在对象中的应用

  • 在一般的对象字面量当中,使用相同的字符串key,字符串会被覆盖。
const classMates = {
         // 字符串 覆盖
        "baby": 1,
        "baby": 2,
}
console.log(classMates) // 输出 {baby: 2}
  • Symbol 最常见的用途是作为对象的键: Symbol 可以代替字符串作为key
 let name = "Tommy";
        const classMates = {
            "baby": 1,
            "baby": 2,
            [name]:"猛男",
            [Symbol('Mark')]:{ grade: 50, gender: "male"},
            [Symbol('olivia')]:{ grade: 80, gender: "female"},
            [Symbol('olivia')]:{ grade: 80, gender: "female"},
        }
        console.log(classMates[name],classMates.baby,classMates);
        // 猛男 2
        // Tommy: "猛男"
        // baby: 2
        // Symbol(Mark): {grade: 50, gender: 'male'}
        // Symbol(olivia): {grade: 80, gender: 'female'}
        // Symbol(olivia): {grade: 80, gender: 'female'}

在这种情况下,使用 Symbol 作为键可以保证无论在何处,键都是唯一的,不会与其他属性发生冲突。

注意,在 JavaScript 中,当我们想要访问对象的属性时,有两种主要的方式:点符号(.)和方括号符号([])。这两种方式有其各自的使用场景和特点。

点符号(.

当你知道属性名,并且这个属性名是一个合法的标识符(即它由字母、数字、下划线或美元符号组成,且不以数字开头),你可以使用点符号来访问该属性。例如:

const obj = {
    name: "Alice",
    age: 30
};
console.log(obj.name); // 输出: Alice
console.log(obj.age);  // 输出: 30

方括号符号([]

方括号符号提供了更大的灵活性。它允许你用字符串或变量来动态地指定属性名。这意味着如果你有一个变量保存了属性名,或者属性名包含空格或其他非法字符,那么你就需要用到方括号符号。方括号内的表达式会被计算,然后将结果作为属性名来查找对应的值。

console.log(classMates[name], classMates.baby);

这里 name 是一个变量,它的值是 "Tommy",所以 classMates[name] 实际上是在尝试获取 classMates 对象中名为 "Tommy" 的属性的值。而 classMates.baby 则是在尝试直接访问名为 "baby" 的属性,如果该属性不存在,则返回 undefined。因此,当你不确定属性名是否为合法标识符,或者是通过变量动态决定的时候,应该使用方括号符号来安全地访问对象的属性。这确保了代码的健壮性和可读性。

Symbol 和属性遍历

当使用常规方法(如 Object.keys()for...in 循环)遍历对象属性时,Symbol 类型的键不会被包含在内。如果要获取对象中的 Symbol 键,必须使用 Object.getOwnPropertySymbols()

const obj = {
    [Symbol('sym1')]: 'value1',
    [Symbol('sym2')]: 'value2'
};
const symbols = Object.getOwnPropertySymbols(obj);
symbols.forEach((sym) => console.log(obj[sym])); // 输出:value1 和 value2

完整的代码示例

我们可以通过使用 Symbol 来创建一个包含学生分数和性别的对象,并确保即使学生名字重复,他们的数据也保持独立且安全:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        // 学生对象字面量
        let name = "Tommy";
        const classMates = {
            "baby": 2,
            [name]: "猛男",
            [symMark]: { grade: 50, gender: "male" },
            [symOlivia]: { grade: 80, gender: "female" }
        };
        console.log(classMates[name], classMates.baby, classMates[symMark], classMates[symOlivia]);

        // 使用 Object.getOwnPropertySymbols 获取 Symbol 类型的键
        const symbols = Object.getOwnPropertySymbols(classMates);
        const grades = symbols.map(sym => classMates[sym].grade); // map方法遍历由 Object.getOwnPropertySymbols 返回的 Symbol 数组
        console.log(grades);

        // 输出属性描述符
       symbols.forEach(sym => {
            console.log(Object.getOwnPropertyDescriptor(classMates, sym));
        });
    </script>
</body>
</html>

可迭代对象与属性描述符

image.png

Object.getOwnPropertySymbols()

Object.getOwnPropertySymbols() 是一个静态方法,它返回一个数组,包含指定对象自身的所有 Symbol 属性(不包括原型链上的属性)。这是获取对象上所有 Symbol 类型属性键的唯一途径,因为 Symbols 默认是不可枚举的(即它们不会出现在 for...in 循环或 Object.keys()Object.values()Object.entries() 的结果中)。

Symbol 的 enumerable 属性

尽管每个 Symbol 都有一个内部的 [[Enumerable]] 属性,默认值为 false,这意味着即使你显式地将一个 Symbol 属性设置为可枚举(例如通过 Object.defineProperty()),它仍然不会在普通的遍历操作中出现。这是因为 Symbol 的设计初衷就是为了提供一种特殊的、独一无二的键,用于避免命名冲突,并且可以在某些情况下保持一定的隐私性。

Object.getOwnPropertyDescriptor():

这是一个静态方法,用于获取指定对象上某个自身属性的属性描述符。属性描述符提供了有关该属性配置的详细信息,如是否可写、是否可枚举、是否可配置等。

用例: 保护对象属性

由于 Symbol 的不可预测性和不可枚举性,它们非常适合用于创建对象的私有属性。当你希望对象的某些属性不被外部 API 轻易访问或修改时,Symbol 是一个好选择。


Symbol 在 JavaScript 中扮演着不可或缺的角色,特别是在处理复杂的对象结构或需要确保属性键唯一性的情况下。通过理解和掌握 Symbols,你可以编写更加健壮和易于维护的代码,特别是在大型应用程序或多团队协作环境中。希望这篇关于 Symbol 的文章能够帮助你更好地理解这一强大工具的魅力。