探索 JavaScript Symbol 的独特魅力与应用在对象字面量中需要注意的细节

引言

在 JavaScript 的世界里,我们总是在追求更高效、更安全的数据处理方式。从 ES6(ECMAScript 2015)开始,JavaScript 引入了一种新的原始数据类型------Symbol。今天,我们将一起探索这个相对神秘的数据类型,了解它背后的故事以及如何在代码中有效利用它。

唯一值

使用 Symbol 函数声明唯一值

Symbol 是一种独一无二且不可变的数据类型,它的值是通过调用 Symbol() 函数创建的。每次调用 Symbol() 都会返回一个全新的、唯一的 Symbol 实例。你也可以选择给 Symbol 提供一个可选的标签(label),但这并不影响其唯一性,主要用于调试和字符串化时的描述。

Javascript 复制代码
const uniqueSymbol = Symbol('unique label');
console.log(uniqueSymbol); // 输出: Symbol(unique label)

返回值为唯一值

每个 Symbol 实例都是独一无二的,即使它们有相同的标签。这意味着即使两个 Symbol 有着相同的描述符,它们也不会相等。

Javascript 复制代码
const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2); // false

在对象字面量中的应用

Symbol 经常被用来作为对象属性的键,这样可以确保该属性不会与对象的其他属性发生冲突,也不需要担心会被覆盖或修改。这对于大型项目特别有用,在多人协作环境中可以避免命名冲突的问题。

Javascript 复制代码
const obj = {
    [Symbol('uniqueKey')]: 'This value is safe from conflicts'
};

// 尝试访问 symbol 键
console.log(obj[Symbol('uniqueKey')]); // undefined, 因为这是另一个新的 Symbol 实例

为什么需要使用方括号 []

在对象字面量中,当你想要使用变量或表达式来定义属性名时,你需要将这个表达式包裹在方括号 [] 中。这是因为对象字面量中的键默认被解释为字符串或标识符,而方括号允许你在运行时计算键名。对于 Symbol 类型的键来说,这是必需的,因为你不能直接用 Symbol 表达式作为静态键名。

Object.keys(), Object.values(), 和 Object.entries()

值得注意的是,Object.keys(), Object.values(), 和 Object.entries() 这些方法无法获取到使用 Symbol 作为键的属性。这是因为 Symbol 类型的键被视为非枚举属性,这进一步保护了以 Symbol 为键名的数据不被意外地遍历或修改。

Javascript 复制代码
const myObj = {
    name1: 'Alice',
    name2: 'Bob', // 修改为英文逗号
    [Symbol('age')]: 30,
    [Symbol('job')]: 'Engineer'
};

console.log(Object.keys(myObj));     // ['name1', 'name2']
console.log(Object.values(myObj));   // ['Alice', 'Bob']
console.log(Object.entries(myObj));  // [['name1', 'Alice'], ['name2', 'Bob']]

请注意,Object.keys(), Object.values(), 和 Object.entries() 方法都只会返回对象上可枚举的字符串类型的键或值,因此它们不会列出任何 Symbol 类型的键或对应的值。如果你想要获取所有的键(包括 Symbol 类型的),你可以使用 Object.getOwnPropertySymbols() 或者结合 Reflect.ownKeys() 方法来获得所有键(包括字符串和符号键)。

这里是如何获取包含 Symbol 键的结果:

Javascript 复制代码
// 获取 Symbol 类型的键
console.log(Object.getOwnPropertySymbols(myObj)); 
// 输出: [Symbol(age), Symbol(job)]

// 获取所有键(包括字符串和 Symbol 类型)
console.log(Reflect.ownKeys(myObj));
// 输出: ['name1', 'name2', Symbol(age), Symbol(job)]

对于 Symbol 类型的键,如果你想访问它们对应的值,你需要直接通过这些 Symbol 实例来访问:

Javascript 复制代码
const ageSym = Object.getOwnPropertySymbols(myObj)[0];
const jobSym = Object.getOwnPropertySymbols(myObj)[1];

console.log(myObj[ageSym]); // 30
console.log(myObj[jobSym]); // Engineer

遍历对象中的 Symbol

当涉及到遍历对象时,传统的 for...in 循环或者 Object.keys() 方法都无法直接访问到 Symbol 类型的键。如果你有一个对象,其中包含了 Symbol 类型的键,并且想要遍历它们,你可以使用 Object.getOwnPropertySymbols() 方法来获取这些键,然后进行遍历。

假设我们有一个包含学生成绩的对象 classMates,其中一些键是 Symbol 类型:

Javascript 复制代码
const classMates = {
    name: 'Alice',
    [Symbol('math')]: 95,
    [Symbol('science')]: 88
};

// 使用 for...in 遍历字符串类型的键
for (const key in classMates) {
    if (classMates.hasOwnProperty(key)) { // 确保只遍历自己的属性
        console.log(key, classMates[key]);
    }
}

// 获取所有 Symbol 类型的键
const syms = Object.getOwnPropertySymbols(classMates);
console.log(syms); // 输出: [Symbol(math), Symbol(science)]

// 使用 map 方法获取 Symbol 键对应的值
const data = syms.map(sym => classMates[sym]);
console.log(data); // 输出: [95, 88]

在这个例子中,for...in 只会遍历字符串类型的键,而不会触及 Symbol 类型的键。通过 Object.getOwnPropertySymbols(),我们可以明确地访问 Symbol 类型的键,进而获取其对应的值。

属性描述符

每个对象的属性都有一个关联的属性描述符,它定义了该属性的行为。例如,是否可以被枚举、是否可配置、是否可以修改等。对于 Symbol 类型的键,我们同样可以通过 Object.getOwnPropertyDescriptor() 来查看或设置它们的描述符。

Javascript 复制代码
// 获取所有自身的属性描述符
console.log(Object.getOwnPropertyDescriptors(classMates));

// 获取特定属性的描述符
console.log(Object.getOwnPropertyDescriptor(classMates, Symbol('math')));// undefined

结果图为:

你可以发现两个问题

1.Symbol显示的是可枚举的

Symbol 类型的键在 JavaScript 中是设计为不可枚举的,这意味着它们不会被包括在对象的默认枚举操作中。这背后的原因是为了提供一种方法来定义不会意外地通过常规属性遍历方式(如 for...in 循环、Object.keys()JSON.stringify() 等)暴露出来的属性。这种设计使得 Symbol 键非常适合用于库和框架开发,因为它们可以避免命名冲突,并且不会干扰用户的代码或第三方库。

为什么 Symbol 是"显示可枚举"的但不能被枚举出来?

这里的"显示可枚举"可能是一个误解。实际上,Symbol 键不是自动可枚举的,而是有特定的行为:

  • 不可枚举Symbol 键默认是不可枚举的,这意味着它们不会出现在 for...infor...of 循环中,也不会被 Object.keys()Object.values()Object.entries() 方法捕捉到。
  • 显示 :尽管 Symbol 键本身不可枚举,你仍然可以通过其他方式"显示"或访问这些键,例如使用 Object.getOwnPropertySymbols() 来获取所有 Symbol 类型的键,或者使用 Reflect.ownKeys() 来获取对象的所有键(包括字符串和 Symbol 类型的键)。

特殊情况

虽然 Symbol 键默认是不可枚举的,但如果你创建了一个带有 Symbol 键的对象并且想要让这个键在某些情况下表现得像可枚举属性一样,你可以这样做:

Javascript 复制代码
const sym = Symbol('description');
const obj = {
    [sym]: 'value',
};

// 默认情况下,Symbol 键是不可枚举的
console.log(Object.keys(obj)); // []
console.log(Object.getOwnPropertyNames(obj)); // []

// 但是你可以明确地获取 Symbol 键
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(description)]

// 如果你需要一个属性描述符来设置 enumerable 属性,可以这样做:
Object.defineProperty(obj, sym, { enumerable: true });

// 然而,即使如此,Symbol 键依然不会出现在 Object.keys() 的结果中
console.log(Object.keys(obj)); // []

值得注意的是,即使你将 enumerable 设置为 trueSymbol 键仍然不会被 Object.keys() 捕捉到。这是因为 Object.keys() 只返回字符串类型的可枚举属性,而 Symbol 键始终被视为非字符串类型的键。

2.Object.getOwnPropertyDescriptor(classMates, Symbol('math'))显示的是undefined

需要注意的是,Object.getOwnPropertyDescriptor() 接受两个参数:目标对象和要查询的属性名。如果我们想获取 Symbol 类型键的描述符,我们需要提供确切的 Symbol 实例作为第二个参数。

那么为什么是undefined呢?

如果你发现 console.log(Object.getOwnPropertyDescriptor(classMates, Symbol('math'))); 输出了 undefined,这通常是因为你传递给 Object.getOwnPropertyDescriptor() 方法的 Symbol 实例与对象中定义的 Symbol 键不匹配。

在 JavaScript 中,每次调用 Symbol() 函数都会创建一个新的、唯一的 Symbol 实例。这意味着即使两个 Symbol 拥有相同的描述(label),它们也是不同的 Symbol 实例,并且不会相等。因此,如果你尝试使用一个新创建的 Symbol 去获取属性描述符,而这个 Symbol 并不是最初用来定义属性的那个实例,那么你会得到 undefined,因为该 Symbol 实际上并不存在于对象中。

示例代码解释

假设你有一个对象 classMates,它包含了一个 Symbol 类型的键:

Javascript 复制代码
const classMates = {
    [Symbol('math')]: 95,
};

// 下面的代码将输出 undefined,因为我们使用了一个新的 Symbol 实例
console.log(Object.getOwnPropertyDescriptor(classMates, Symbol('math')));

解决方法

为了正确地获取属性描述符,你需要确保使用的 Symbol 实例是最初定义属性时的那个实例。你可以通过几种方式来实现这一点:

  1. 保存 Symbol 实例 :将创建的 Symbol 保存在一个变量中,并在需要的时候使用这个变量。

    Javascript 复制代码
    const mathSym = Symbol('math');
    const classMates = {
        [mathSym]: 95,
    };
    
    console.log(Object.getOwnPropertyDescriptor(classMates, mathSym));
  2. 使用全局符号注册表 :如果多个地方需要访问同一个 Symbol,可以使用 Symbol.for(key) 来从全局符号注册表中获取 Symbol

    Javascript 复制代码
    const mathSym = Symbol.for('math');
    const classMates = {
        [mathSym]: 95,
    };
    
    console.log(Object.getOwnPropertyDescriptor(classMates, Symbol.for('math')));
  3. 遍历所有 Symbol :如果你不知道具体的 Symbol 实例,可以通过 Object.getOwnPropertySymbols() 获取所有 Symbol 键,然后检查这些键中的哪一个是你想要的。

    Javascript 复制代码
    const classMates = {
        [Symbol('math')]: 95,
    };
    
    const syms = Object.getOwnPropertySymbols(classMates);
    syms.forEach(sym => {
        console.log(Object.getOwnPropertyDescriptor(classMates, sym));
    });

总结

Symbol 提供了新的可能性,让我们的代码更加清晰和安全。它不仅解决了命名冲突的问题,还为开发者提供了更多的灵活性。无论你是初学者还是经验丰富的开发人员,掌握 Symbol 的使用都能让你的编程技能更上一层楼。

相关推荐
菜鸟一枚在这1 小时前
深入解析设计模式之单例模式
开发语言·javascript·单例模式
CL_IN1 小时前
企业数据集成:实现高效调拨出库自动化
java·前端·自动化
浪九天2 小时前
Vue 不同大版本与 Node.js 版本匹配的详细参数
前端·vue.js·node.js
qianmoQ3 小时前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
C#Thread3 小时前
C#上位机--流程控制(IF语句)
开发语言·javascript·ecmascript
椰果uu3 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
微wx笑4 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄4 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器
CUIYD_19894 小时前
Chrome 浏览器(版本号49之后)‌解决跨域问题
前端·chrome
IT、木易4 小时前
跟着AI学vue第五章
前端·javascript·vue.js