【JS八股文】ES6的Symbol数据类型

231 阅读5分钟

一、背景故事:为何我们需要 Symbol?

在编程的世界里,有一种痛苦的现象——命名冲突。想象一下,在一个巨大的多人协作项目中,每个开发者都在用自己的方式修改一个对象:

const user = {
  name: 'Alice',
  id: 123,
  email: 'alice@example.com'
};

// 另一个模块中修改同一个对象
const user = {
  name: 'Bob',
  id: 456,
  email: 'bob@example.com'
};

糟糕了! 这就像是两位程序员在没有沟通的情况下都叫了同一个变量 user,他们互相覆盖了对方的工作。就像两个人都在同一房间里用相同的名字叫“Alan”,结果大家都困惑了:“谁才是Alan?!”

而这里的痛点就是,属性名(比如 id)是全局的,可能会被其他开发者覆盖,导致意外的错误。但还好ES6 提出了一个新的数据类型Symbol —— 它是 JavaScript 中的隐形超级英雄,能帮你解决命名冲突!

通过使用 Symbol,我们可以确保每个属性名都是独一无二的。即使两个 Symbol 有相同的描述,它们依然是两个不一样的对象(就像是两个同名但互不相识的“Alan)

a2a1ed37af06825090e6cdaceb646a7.jpg

二、Symbol的特点:独一无二的存在

1. 唯一性:每一个 Symbol 都是独一无二的

Symbol 是 ES6 引入的一种原始数据类型,其最显著的特性就是 唯一性。每个通过 Symbol 创建的值都是唯一的,即使它们有相同的描述,它们依然是不同的。

const wes = Symbol('Wes');
let name = "张三";

const classMates = {
  // 字符串的属性会被覆盖
  "cy": 1,
  "cy": 2,
  [name]: "猛男",  // 使用变量作为属性名
  [Symbol('Mark')]: { grade: 50, gender: 'male' },  // 使用 Symbol 作为对象的键
  [Symbol('olivia')]: { grade: 80, gender: 'female' },
  [Symbol('Mark')]: { grade: 50, gender: 'male' },  // Symbol 本身是唯一的,无法覆盖
};

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

image.png 解释

唯一性: 你看到,尽管 classMates 对象中有多个同名的属性(如 "cy"),后面的 "cy": 2 会覆盖前面的 "cy": 1,而 Symbol 则不会。即便你给两个 Symbol 相同的描述(如这里定义了两个[Symbol(Mark)]键),它们依然是唯一的,因此不会互相覆盖,这就是为什么它们可以作为对象的唯一标识符,避免属性冲突。

2. 属性描述符:Symbol 的额外信息

对于对象的每个属性(包括 Symbol 类型的属性),你可以通过 Object.getOwnPropertyDescriptors() 获取更多的信息,如属性是否可枚举、是否可以修改等。

console.log(Object.getOwnPropertyDescriptors(classMates));

输出

{
  name: { value: '猛男', writable: true, enumerable: true, configurable: true },
  cy: { value: 2, writable: true, enumerable: true, configurable: true },
  [Symbol('Mark')]: { value: { grade: 50, gender: 'male' }, writable: true, enumerable: false, configurable: true },
  [Symbol('olivia')]: { value: { grade: 80, gender: 'female' }, writable: true, enumerable: false, configurable: true }
}

解释Object.getOwnPropertyDescriptors() 返回了属性的详细描述符。Symbol 创建的属性具有 enumerable: false 特性,这意味着它们不会被常规的枚举方法(如 Object.keys()for...in)访问到,但依然存在于对象中,且可以通过 Object.getOwnPropertySymbols() 获取。

如下

3.不可枚举性:不被 Object.keys() 等枚举方法访问

const mySymbol = Symbol('mySymbol');

// 创建一个包含 Symbol 类型属性的对象
const obj = {
  [mySymbol]: 'This is a symbol property',
  name: 'Alice',
  age: 25
};

// 使用 for...in 遍历对象
for (let key in obj) {
  console.log(key);  // 不会输出 Symbol 类型的键
}

// 使用 Object.keys() 获取所有属性名
console.log(Object.keys(obj));  // 只会输出普通的字符串属性 ['name', 'age']

// 使用 Object.entries() 获取键值对数组
console.log(Object.entries(obj));  // 只会输出普通的键值对 [['name', 'Alice'], ['age', 25]]

// 使用 Object.getOwnPropertySymbols() 获取 Symbol 键
const symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols);  // 输出 [Symbol(mySymbol)]

// 使用该 Symbol 键来访问对应的值
console.log(obj[symbols[0]]);  // 输出 "This is a symbol property"

输出

[]
["name", "age"]
[["name", "Alice"], ["age", 25]]
[Symbol(mySymbol)]
"This is a symbol property"

解释:通过 Object.keys()Object.entries(),我们只能访问到普通的字符串类型的属性,而 Symbol 类型的属性不会被这些方法遍历到。如果想访问 Symbol 类型的属性,可以使用 Object.getOwnPropertySymbols(),返回一个包含对象所有 Symbol 键的数组。

三、Symbol的用途:为什么要使用 Symbol?

1. 避免属性冲突

在多人协作的大型项目中,不同模块可能会使用相同的属性名,这时使用 Symbol 可以避免这种命名冲突,因为每个 Symbol 都是唯一的。

const sym1 = Symbol('uniqueKey');
const sym2 = Symbol('uniqueKey');

const obj = {
  [sym1]: 'value1',
  [sym2]: 'value2'
};

console.log(obj[sym1]);  // "value1"
console.log(obj[sym2]);  // "value2"

解释:即使两个 Symbol 的描述相同,它们依然是不同的,确保了对象中属性的唯一性,避免了命名冲突。

2. 定义私有属性

Symbol 还可以用来模拟私有属性,这些属性外部代码无法直接访问,这对封装和数据保护非常有用。

const _private = Symbol('private');

class MyClass {
  constructor() {
    this[_private] = 'private data';
  }

  getPrivateData() {
    return this[_private];
  }
}

const instance = new MyClass();
console.log(instance.getPrivateData());  // "private data"

解释:使用 Symbol 可以有效模拟私有属性,防止外部直接访问这些属性。这在封装类的内部数据时非常有用。

3. 定义对象的自定义迭代器

通过 Symbol.iterator,我们可以定义一个对象的自定义遍历行为,使得该对象可以像数组一样被 for...of 遍历。

const myArray = {
  0: 'apple',
  1: 'banana',
  length: 2,
  [Symbol.iterator]: function () {
    let index = 0;
    const data = this;
    return {
      next: function () {
        if (index < data.length) {
          return { value: data[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for (let fruit of myArray) {
  console.log(fruit);
}
// 输出:
// apple
// banana

解释:通过实现 Symbol.iterator,我们为 myArray 对象定义了一个自定义的遍历器,使得它可以在 for...of 循环中被使用。

四、总结:Symbol—让你的代码更优雅、安全、可维护

  • Symbol 提供了一种独一无二的方式来创建对象的键,避免了命名冲突,尤其在多人协作的环境中尤为重要。
  • 它还能够模拟私有属性,增强对象的封装性。
  • 通过实现 Symbol.iterator,你可以让对象支持自定义的遍历行为,使得对象能够像数组一样被遍历。
  • Symbol 的不可枚举特性,使得它非常适合用于存储不希望被枚举的属性。

在复杂的应用或大型项目中,Symbol 是你不可或缺的工具。它不仅能解决命名冲突,还能让你的代码更安全、可维护。它的存在,让你在 JavaScript 世界中拥有了一种隐形的超级力量,让你能够以更加优雅的方式控制对象的属性和行为。