引言
在 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...in
或for...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
设置为 true
,Symbol
键仍然不会被 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
实例是最初定义属性时的那个实例。你可以通过几种方式来实现这一点:
-
保存
Symbol
实例 :将创建的Symbol
保存在一个变量中,并在需要的时候使用这个变量。Javascriptconst mathSym = Symbol('math'); const classMates = { [mathSym]: 95, }; console.log(Object.getOwnPropertyDescriptor(classMates, mathSym));
-
使用全局符号注册表 :如果多个地方需要访问同一个
Symbol
,可以使用Symbol.for(key)
来从全局符号注册表中获取Symbol
。Javascriptconst mathSym = Symbol.for('math'); const classMates = { [mathSym]: 95, }; console.log(Object.getOwnPropertyDescriptor(classMates, Symbol.for('math')));
-
遍历所有
Symbol
键 :如果你不知道具体的Symbol
实例,可以通过Object.getOwnPropertySymbols()
获取所有Symbol
键,然后检查这些键中的哪一个是你想要的。Javascriptconst classMates = { [Symbol('math')]: 95, }; const syms = Object.getOwnPropertySymbols(classMates); syms.forEach(sym => { console.log(Object.getOwnPropertyDescriptor(classMates, sym)); });
总结
Symbol
提供了新的可能性,让我们的代码更加清晰和安全。它不仅解决了命名冲突的问题,还为开发者提供了更多的灵活性。无论你是初学者还是经验丰富的开发人员,掌握 Symbol
的使用都能让你的编程技能更上一层楼。