发布于 2026-01-06 2 阅读
0

谁需要JavaScript符号?

谁需要JavaScript符号?

封面图片来自PixabayAlexander Fradellafra 。

Symbol是 JavaScript 中一种不太为人所知的原始数据类型string, number, bigint, boolean and undefined。它们是 ES6 规范的一部分,ES6 规范对 JavaScript 语言进行了一次重大革新,并包含了许多新特性。

我们为什么需要符号?

符号主要有两种用途:

  1. 在对象上创建隐藏属性,其他任何代码(未引用所用符号的代码)都无法访问或覆盖这些属性。大多数内置函数和库的惯例是,除非直接需要修改,否则避免引用对象上声明的符号。

  2. 用于改变对象默认行为的系统符号——例如,Symbol.toPrimitive用于在对象转换为基本类型期间定义对象行为的符号,或者Symbol.iterator用于在迭代期间设置对象行为的符号。

符号基础知识

符号的语法非常 象征很简单。我们可以通过编写以下代码来创建一个新符号:

// mySymbol is a new created symbol
let mySymbol = Symbol();
console.log(mySymbol) // Symbol()
Enter fullscreen mode Exit fullscreen mode

Symbol() 函数有一个可选的描述字段,可以这样使用:

// mySymbol is a new created symbol that now has a description
let mySymbol = Symbol('decription of my symbol');
console.log(mySymbol) // Symbol(decription of my symbol)
Enter fullscreen mode Exit fullscreen mode

描述字段只是一段将附加到符号上的文本——它主要用于调试目的。

Symbol() 函数返回的每个符号都是唯一的,这意味着使用该函数创建的两个符号永远不会相等(即使它们传递给该函数的描述相同):

let firstSymbol = Symbol("sameDescription");
let secondSymbol = Symbol("sameDescription");
console.log(firstSymbol == secondSymbol); //false
Enter fullscreen mode Exit fullscreen mode

在对象中创建隐藏属性

现在我们知道了如何创建一个新的符号,让我们看看如何使用它来创建对象的隐藏属性。

替代文字

首先——我们为什么要这样做?

举个常见的例子,当我们的代码被第三方使用时,就会遇到这种情况。例如,我们正在编写一个开源库,或者一个将被我们组织内其他开发团队使用的库。我们可能需要在对象中添加一些“底层”属性,以便在我们的代码中访问它们——但同时,我们也希望确保其他任何代码都无法访问这些属性。

如果我们使用字符串声明的常规对象属性,那么使用我们库的开发人员可能会通过遍历对象键或创建同名属性并覆盖它而意外地这样做。

符号是用来帮助我们的。

例如——假设我们有一个代表摇滚明星的对象:

let rockStar = {
  name: "James Hetfield",
  band: "Metallica",
  role: "Voice & Rythm guitar"
}
Enter fullscreen mode Exit fullscreen mode

现在我们想添加一个隐藏属性,该属性表示一个内部 ID,我们希望该 ID 仅在我们的代码中公开,并避免在内部代码之外使用它:

let idSymbol = Symbol('id symbol used in rockStar object');

let rockStar = {
  name: "James Hetfield",
  band: "Metallica",
  role: "Voice & Rythm guitar"
  [idSymbol]: "this-id-property-is-set-by-symbol"
}
Enter fullscreen mode Exit fullscreen mode

如果我们现在想要访问、更改或删除使用符号设置的属性,我们需要拥有用于声明该属性的符号的引用。如果没有该引用,我们就无法进行这些操作。

此外,当遍历对象的键时,我们将无法获得对使用 Symbol 设置的属性的引用:

console.log(Object.keys(rockStar)); // (3) ["name", "band", "role"]
Enter fullscreen mode Exit fullscreen mode

for ... in ...循环也会忽略我们的符号:

for (key in rockStar) {
    console.log(key);
}

// output:
// name
// band
// role

Enter fullscreen mode Exit fullscreen mode

全球符号注册表

如果在某些情况下,我们确实需要添加对使用符号定义的属性的访问权限,该怎么办?如果我们需要在应用程序的不同模块之间共享对这些属性的访问权限,又该怎么办?

这时全局符号注册表就派上用场了。你可以把它想象成一个全局字典——在代码的任何位置,我们都可以通过特定的键来设置或获取符号。

Symbol.for是一种用于从全局注册表中获取符号的语法。

让我们沿用同样的例子,并使用全局注册表重写一遍:

let idSymbol = Symbol.for('rockStarIdSymbol');

let rockStar = {
  name: "James Hetfield",
  band: "Metallica",
  role: "Voice & Rythm guitar"
  [idSymbol]: "this-id-property-is-set-by-symbol"
}
Enter fullscreen mode Exit fullscreen mode

let idSymbol = Symbol.for('rockStarIdSymbol');将执行以下操作:

  1. 检查全局注册表中是否存在与键值相等的符号rockStarIdSymbol,如果存在,则返回该符号。
  2. 如果不行,则创建一个新符号,将其存储在注册表中并返回。

这意味着,如果我们需要在代码中的其他任何地方访问我们的属性,我们可以这样做:

let newSymbol = Symbol.for('rockStarIdSymbol');
console.log(rockStar[newSymbol]); // "this-id-property-is-set-by-symbol"
Enter fullscreen mode Exit fullscreen mode

因此——值得一提的是——全局注册表中同一键返回的两个不同的符号是相等的:

let symbol1 = Symbol.for('rockStarIdSymbol');
let symbol2 = Symbol.for('rockStarIdSymbol');
console.log(symbol1 === symbol2); // true
Enter fullscreen mode Exit fullscreen mode

还可以使用Symbol.keyFor函数来检查 Symbol 与全局注册表中的哪个键相关联。

const symbolForRockstar = Symbol.for('rockStarIdSymbol')
console.log(Symbol.keyFor(symbolForRockstar)); //rockStarIdSymbol
Enter fullscreen mode Exit fullscreen mode

Symbol.keyFor正在检查全局注册表并查找符号的键。如果该符号未在注册表中注册,undefined则返回空值。

系统符号

系统符号是可用于自定义对象行为的符号。完整的系统符号列表可在最新的语言规范中找到。每个系统符号都提供对某些规范的访问,我们可以覆盖和自定义这些规范的行为。

替代文字

举例来说——让我们来看一个常用符号的用法——Symbol.iterator它使我们能够访问iterator规范。

假设我们要编写一个代表乐队的 JavaScript 类。
它可能包含乐队名称、风格和乐队成员列表。

class Band {
   constructor(name, style, members) {
     this.name = name;
     this.style = style;
     this.members = members;
   }
}
Enter fullscreen mode Exit fullscreen mode

我们可以通过编写类似这样的代码来创建该类的新实例:

const metallicaBand = new Band('Metallica', 'Heavy metal', 
['James', 'Lars', 'Kirk', 'Robert'];
Enter fullscreen mode Exit fullscreen mode

如果我们希望用户能够像遍历数组一样遍历类实例并获取乐队成员的姓名,该怎么办?一些库中也采用了这种行为,它们将数组封装在对象中。

现在,如果我们尝试使用for ... of循环遍历对象,将会收到一个错误Uncaught TypeError: "metallicaBand" is not iterable。这是因为我们的类定义中没有关于如何进行迭代的指令。如果我们确实想要启用迭代功能,我们需要设置相应的行为,而 `Symbol.iterator` 就是一个我们应该使用的系统符号。

让我们把它添加到类定义中:

class Band {
   constructor(name, style, members) {
     this.name = name;
     this.style = style;
     this.members = members;
   }

  [Symbol.iterator]() { 
    return new BandIterator(this);
  }
}

class BandIterator{
  // iterator implementation
}
Enter fullscreen mode Exit fullscreen mode

我不会深入探讨迭代器的具体实现——这可以另写一篇文章来详细讲解。但说到符号(Symbols),这正是我们应该了解的用例。几乎所有原生行为都可以被修改,而系统符号就是在 JavaScript 类中实现这一目标的方法。

还有什么?

1)严格来说,使用符号设置的对象的属性并非完全隐藏。有一些方法Object.getOwnPropertySymbols(obj)可以返回对象上设置的所有符号,也Reflect.ownKeys(obj)可以列出对象的所有属性,包括符号属性。但通常的做法是不要使用这些方法进行列表、迭代以及对对象执行任何其他通用操作。

2)我偶尔会看到一些代码中使用符号来声明枚举值,例如:

const ColorEnum = Object.freeze({
  RED: Symbol("RED"), 
  BLUE: Symbol("BLUE")
});
Enter fullscreen mode Exit fullscreen mode

我不确定这种做法是否妥当。假设 Symbol 对象不可序列化,那么任何尝试将这些值字符串化的尝试都会导致它们从对象中被移除。

使用符号时,务必谨慎使用序列化。总而言之,请避免使用深度拷贝JSON.parse(JSON.stringify(...))。这种做法有时会导致难以发现的 bug,让人彻夜难眠!

3) 用于浅层对象克隆的函数——Object.assign同时复制符号和常规字符串属性。这听起来像是一个合理的设计行为。

我认为关于符号,你只需要知道这些就能全面了解它们了。我漏掉了什么吗?

替代文字

很高兴你坚持到了这里!

感谢阅读,和往常一样,欢迎提出任何反馈意见。

如果你像我一样热爱 Javascript,请访问https://watcherapp.online/ —— 这是我的一个副项目,将所有 Javascript 博客文章集中在一个地方,里面有很多有趣的内容!

文章来源:https://dev.to/shadowwarior5/who-needs-javascript-symbols-4g6l