JS `Symbol`:创建唯一标识符,防止属性名冲突

各位观众老爷,大家好!今天咱们来聊聊 JavaScript 里一个有点神秘,但又贼好用的东西——Symbol。这玩意儿,说白了,就是用来创建唯一标识符的,防止你的代码里属性名打架的。

一、啥是 Symbol?为啥要有它?

想象一下,你在开发一个大型的 JavaScript 应用,里面用了各种各样的第三方库。这些库可能也会往你的对象里添加一些属性。如果它们用的属性名跟你用的重了,那可就麻烦了,轻则数据被覆盖,重则程序崩溃。

Symbol 的出现就是为了解决这个问题。它能保证你创建的每一个 Symbol 都是独一无二的,就像每个人都有一个唯一的身份证号一样。

简单来说,Symbol 是一种新的原始数据类型(primitive data type),跟 NumberStringBooleanNullUndefinedBigInt 这些哥们儿是平起平坐的。

二、怎么创建 Symbol

创建 Symbol 非常简单,直接调用 Symbol() 函数就行了。

const mySymbol = Symbol();
console.log(typeof mySymbol); // "symbol"

你也可以给 Symbol 加上一个描述,方便调试和阅读。

const mySymbolWithDescription = Symbol('这是一个描述');
console.log(mySymbolWithDescription.description); // "这是一个描述"

注意,Symbol 函数前面不能用 new 关键字。用了就报错,不信你试试:

// 错误示例
// const mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

三、Symbol 的特性:独一无二

最关键的一点是,每次调用 Symbol() 函数,都会返回一个全新的、独一无二的 Symbol。即使你用相同的描述来创建 Symbol,它们也是不同的。

const symbol1 = Symbol('相同描述');
const symbol2 = Symbol('相同描述');

console.log(symbol1 === symbol2); // false

四、Symbol 的应用场景

  1. 作为对象属性名:防止属性名冲突

这是 Symbol 最常见的用途。你可以用 Symbol 作为对象的属性名,这样可以保证你的属性名不会跟其他代码冲突。

const mySymbol = Symbol();

const myObject = {
  [mySymbol]: '这是一个秘密属性'
};

console.log(myObject[mySymbol]); // "这是一个秘密属性"

注意:用 Symbol 作为属性名时,要用方括号 [] 包裹起来。

  1. 模拟私有属性

虽然 JavaScript 没有真正的私有属性,但你可以用 Symbol 来模拟。因为 Symbol 属性不容易被访问到,所以可以起到一定的保护作用。

class MyClass {
  #privateSymbol = Symbol(); // 注意这里用了#,是ES2022的私有字段语法,更容易实现真正的私有,但下面我们还是主要围绕Symbol展开

  constructor(value) {
    this[this.#privateSymbol] = value;
  }

  getValue() {
    return this[this.#privateSymbol];
  }
}

const myInstance = new MyClass('敏感数据');
console.log(myInstance.getValue()); // "敏感数据"
// console.log(myInstance[privateSymbol]); // 无法直接访问,因为作用域不同,而且无法拿到这个Symbol
  1. 定义常量:替代字符串或数字

你可以用 Symbol 来定义常量,这样可以提高代码的可读性和可维护性。

const STATUS_PENDING = Symbol('pending');
const STATUS_RUNNING = Symbol('running');
const STATUS_FINISHED = Symbol('finished');

function processTask(status) {
  switch (status) {
    case STATUS_PENDING:
      console.log('任务等待中');
      break;
    case STATUS_RUNNING:
      console.log('任务正在运行');
      break;
    case STATUS_FINISHED:
      console.log('任务已完成');
      break;
    default:
      console.log('未知状态');
  }
}

processTask(STATUS_RUNNING); // "任务正在运行"
  1. 作为迭代器的标识符

Symbol.iterator 是一个预定义的 Symbol,用于指定一个对象是否可迭代。

const myIterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (const item of myIterable) {
  console.log(item); // 1 2 3
}
  1. 作为元编程的钩子

Symbol 还可以用于元编程,比如自定义对象的行为。JavaScript 提供了一些预定义的 Symbol,可以让你修改对象的默认行为。例如:

*   `Symbol.hasInstance`:  用于自定义 `instanceof` 运算符的行为。
*   `Symbol.toPrimitive`:  用于自定义对象在类型转换时的行为。
*   `Symbol.toStringTag`:  用于自定义对象的 `toString()` 方法返回的字符串。

五、Symbol 的局限性

  1. Symbol 属性不容易被枚举

Symbol 属性不会被 for...in 循环、Object.keys()Object.getOwnPropertyNames() 方法枚举出来。

const mySymbol = Symbol();

const myObject = {
  name: '张三',
  [mySymbol]: '这是一个秘密属性'
};

for (const key in myObject) {
  console.log(key); // "name"
}

console.log(Object.keys(myObject)); // ["name"]
console.log(Object.getOwnPropertyNames(myObject)); // ["name"]
  1. Symbol 属性可以通过 Object.getOwnPropertySymbols() 方法获取

虽然 Symbol 属性不容易被枚举,但你可以通过 Object.getOwnPropertySymbols() 方法获取对象的所有 Symbol 属性。

const mySymbol = Symbol();

const myObject = {
  name: '张三',
  [mySymbol]: '这是一个秘密属性'
};

const symbolKeys = Object.getOwnPropertySymbols(myObject);
console.log(symbolKeys); // [Symbol()]
console.log(myObject[symbolKeys[0]]); // "这是一个秘密属性"
  1. 全局 Symbol 注册表

JavaScript 提供了一个全局 Symbol 注册表,你可以通过 Symbol.for() 方法来创建或获取全局 Symbol

const globalSymbol = Symbol.for('myGlobalSymbol');
const anotherGlobalSymbol = Symbol.for('myGlobalSymbol');

console.log(globalSymbol === anotherGlobalSymbol); // true
console.log(Symbol.keyFor(globalSymbol)); // "myGlobalSymbol"

注意:Symbol.for() 方法会先检查全局注册表中是否已经存在相同描述的 Symbol。如果存在,就返回已存在的 Symbol;如果不存在,就创建一个新的 Symbol 并将其添加到全局注册表中。

六、Symbol 相关的 API

API 描述
Symbol() 创建一个新的 Symbol
Symbol.for(key) 在全局 Symbol 注册表中查找或创建一个 Symbol
Symbol.keyFor(symbol) 返回全局 Symbol 注册表中与指定 Symbol 关联的键。
Symbol.hasInstance 用于自定义 instanceof 运算符的行为。
Symbol.iterator 用于指定一个对象是否可迭代。
Symbol.toPrimitive 用于自定义对象在类型转换时的行为。
Symbol.toStringTag 用于自定义对象的 toString() 方法返回的字符串。
Object.getOwnPropertySymbols(obj) 返回一个数组,包含指定对象的所有 Symbol 属性。

七、Symbol 的一些使用建议

  • 尽量使用描述:Symbol 添加描述可以提高代码的可读性和可维护性。
  • 谨慎使用全局 Symbol 全局 Symbol 可能会导致命名冲突,所以要谨慎使用。
  • 不要滥用 Symbol Symbol 主要用于解决属性名冲突的问题,不要滥用。

八、Symbol 的一些实际例子

例子 1:防止第三方库修改你的对象

假设你使用了一个第三方库,它会往你的对象里添加一个 id 属性。为了防止冲突,你可以用 Symbol 来定义自己的 id 属性。

// 第三方库
const thirdPartyLibrary = {
  addId: (obj) => {
    obj.id = 'third-party-id';
  }
};

// 你的代码
const mySymbol = Symbol('myId');

const myObject = {
  name: '我的对象',
  [mySymbol]: 'my-unique-id'
};

thirdPartyLibrary.addId(myObject);

console.log(myObject.id); // "third-party-id" (第三方库覆盖了你的属性,但是没关系,你的还在)
console.log(myObject[mySymbol]); // "my-unique-id" (你的属性依然存在)

例子 2:控制对象的可迭代性

你可以用 Symbol.iterator 来控制对象的可迭代性。

const myObject = {
  name: '我的对象',
  data: [1, 2, 3],
  [Symbol.iterator]: function* () {
    for (const item of this.data) {
      yield item;
    }
  }
};

for (const item of myObject) {
  console.log(item); // 1 2 3
}

// 如果你不想让对象可迭代,可以这样:
const myNonIterableObject = {
  name: '我的不可迭代对象',
  data: [1, 2, 3]
};

// 尝试迭代会报错
// for (const item of myNonIterableObject) { // TypeError: myNonIterableObject is not iterable
//   console.log(item);
// }

九、总结

Symbol 是 JavaScript 中一个非常有用的特性,它可以用来创建唯一标识符,防止属性名冲突,模拟私有属性,定义常量,以及进行元编程。虽然 Symbol 有一些局限性,但只要你合理使用,就能提高代码的可读性、可维护性和安全性。

希望今天的讲座对大家有所帮助!下次有机会再跟大家聊聊其他 JavaScript 的奇技淫巧。 感谢大家的收听,再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注