字符基元是ES6标准的创新之一,它为JavaScript带来了一些有价值的功能。 当用作对象属性的标识符时,由Symbol数据类型表示的符号特别有用。 结合其应用的这种情况,出现了一个问题,即它们可以做什么,哪些行不能做。

在我们今天发布的翻译材料中,我们将讨论JavaScript中的Symbol数据类型。 我们将首先回顾一些您需要导航以处理符号的JavaScript功能。
初步资料
实际上,在JavaScript中,有两种值。 第一种是原始值,第二种是对象(它们还包含函数)。 基本值包括简单的数据类型,例如数字(包括从整数到浮点数,
Infinity
和
NaN
值的所有内容),逻辑值,字符串,
undefined
和
null
值。 请注意,在检查
typeof null === 'object'
产生
true
,
null
是原始值。
原始值是不可变的。 它们无法更改。 当然,您可以在存储原始值的变量中编写新内容。 例如,这将新值写入变量
x
:
let x = 1; x++;
但是同时,原始数值
1
没有变化(变异)。
在某些语言中,例如在C语言中,存在通过引用和值传递函数自变量的概念。 JavaScript也有类似的东西。 数据工作的确切组织方式取决于其类型。 如果将某个变量表示的原始值传递给该函数,然后在该函数中对其进行更改,则存储在原始变量中的值不会更改。 但是,如果将变量代表的对象值传递给函数并对其进行修改,则此变量中存储的内容也会更改。
考虑以下示例:
function primitiveMutator(val) { val = val + 1; } let x = 1; primitiveMutator(x); console.log(x);
原始值(神秘的
NaN
除外,它不等于自身)总是与其他看起来像它们自己的原始值相等。 例如:
const first = "abc" + "def"; const second = "ab" + "cd" + "ef"; console.log(first === second);
但是,从外部看相同的对象值的构造不会导致获得实体的事实,当比较时,将揭示它们之间的相等性。 您可以通过以下方式对此进行验证:
const obj1 = { name: "Intrinsic" }; const obj2 = { name: "Intrinsic" }; console.log(obj1 === obj2);
对象在JavaScript中起着基本作用。 它们几乎在任何地方都可以使用。 例如,它们通常以键/值集合的形式使用。 但是在
Symbol
数据类型出现之前,只能将字符串用作对象键。 这是对使用集合形式的对象的严重限制。 尝试将非字符串值分配为对象键时,此值已强制转换为字符串。 您可以通过以下方式对此进行验证:
const obj = {}; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[{}] = 'someobj'; console.log(obj);
顺便说一句,尽管这使我们与字符的主题有些距离,但我想指出,创建
Map
数据结构是为了在键不是字符串的情况下允许使用键/值数据存储。
什么是符号?
现在我们已经弄清了JavaScript中原始值的功能,我们终于可以开始讨论字符了。 符号是唯一的原始含义。 如果从该位置接近符号,则将注意到这方面的符号与对象相似,因为创建符号的多个实例将导致创建不同的值。 但是,符号是不可变的原始值。 这是使用字符的示例:
const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2);
创建字符实例时,可以使用可选的第一个字符串参数。 此自变量是旨在用于调试的符号的描述。 该值不影响符号本身。
const s1 = Symbol('debug'); const str = 'debug'; const s2 = Symbol('xxyy'); console.log(s1 === str);
符号作为对象属性的键
符号可以用作对象的属性键。 这很重要。 这是这样使用它们的示例:
const obj = {}; const sym = Symbol(); obj[sym] = 'foo'; obj.bar = 'bar'; console.log(obj);
请注意,
Object.keys()
方法时,不会返回由字符指定的键。 在JS中出现字符之前编写的代码对它们一无所知,因此,古代的
Object.keys()
方法不应返回有关字符所表示对象的键的信息。
乍一看,似乎字符的上述功能使您可以使用它们来创建JS对象的私有属性。 在许多其他编程语言中,可以使用类创建隐藏的对象属性。 长期以来,缺少此功能一直被认为是JavaScript的缺点之一。
不幸的是,与对象一起使用的代码可以自由访问其字符串键。 此外,该代码还可以访问由字符指定的键,即使它们与对象一起使用的代码无法访问相应的字符。 例如,使用
Reflect.ownKeys()
方法,您可以获得对象的所有键的列表,这些键既是字符串,又是字符:
function tryToAddPrivate(o) { o[Symbol('Pseudo Private')] = 42; } const obj = { prop: 'hello' }; tryToAddPrivate(obj); console.log(Reflect.ownKeys(obj));
请注意,目前正在努力使类具有使用私有属性的能力。 此功能称为“
专用字段” 。 的确,它并不会绝对影响所有对象,而只是引用基于先前准备的类创建的对象。 Chrome浏览器72版及更早版本已提供对私有字段的支持。
防止对象属性名称冲突
当然,符号不会为JavaScript添加创建对象私有属性的功能,但是由于其他原因,它们是该语言中的宝贵创新。 即,它们在某些库需要向在其外部描述的对象添加属性,同时又不怕对象的属性名称冲突的情况下很有用。
考虑一个示例,其中两个不同的库希望将元数据添加到对象。 两个库都可能需要为对象配备一些标识符。 如果仅使用诸如两个字母的
id
字符串之类的名称作为此类属性的名称,则可能会遇到一种情况,即一个库会覆盖另一个库所指定的属性。
function lib1tag(obj) { obj.id = 42; } function lib2tag(obj) { obj.id = 369; }
如果在示例中使用符号,则每个库都可以在初始化时生成其所需的符号。 然后,可以使用这些符号将属性分配给对象并访问这些属性。
const library1property = Symbol('lib1'); function lib1tag(obj) { obj[library1property] = 42; } const library2property = Symbol('lib2'); function lib2tag(obj) { obj[library2property] = 369; }
通过查看这种情况,您可以从JavaScript中的字符外观中受益。
但是,关于将库用于对象,随机字符串或具有复杂结构的字符串的属性的名称可能存在问题,例如包括库的名称。 类似的字符串可以形成类似于库使用的标识符的名称空间的名称。 例如,它可能看起来像这样:
const library1property = uuid();
通常,您可以这样做。 实际上,类似的方法与使用符号时的情况非常相似。 而且,如果使用随机标识符或名称空间,几个库不会偶然生成相同的属性名称,那么这些名称就不会有问题。
精明的读者现在会说,正在考虑的两种命名对象属性的方法并不完全等效。 随机生成或使用命名空间生成的属性名称有一个缺点:对应的键很容易找到,尤其是当代码搜索对象的键或将其序列化时。 考虑以下示例:
const library2property = 'LIB2-NAMESPACE-id';
如果在这种情况下将符号用作键名,则对象的JSON表示将不包含符号值。 为什么会这样呢? 事实是,JavaScript中出现了新的数据类型,并不意味着已经对JSON规范进行了更改。 JSON仅支持字符串作为属性键。 序列化对象时,不会尝试以任何特殊方式表示字符。
通过使用
Object.defineProperty()
可以解决在对象的JSON表示形式中获取属性名称的问题:
const library2property = uuid();
通过将其
enumerable
描述符设置为
false
来“隐藏”的字符串键的行为与字符表示的键几乎相同。 当调用
Object.keys()
时,它们都不显示,并且都可以使用
Reflect.ownKeys()
来检测。 看起来是这样的:
const obj = {}; obj[Symbol()] = 1; Object.defineProperty(obj, 'foo', { enumberable: false, value: 2 }); console.log(Object.keys(obj));
我必须说,在这里,我们几乎使用JS的其他方式重新创建了符号的可能性。 特别是,用符号表示的密钥和专用密钥都不属于对象的JSON表示形式。 两者都可以通过引用
Reflect.ownKeys()
方法来识别。 结果,它们两个都不能真正称为私有。 如果我们假设使用一些随机值或库名称空间来生成键名,那么这意味着我们摆脱了名称冲突的风险。
但是,使用符号名称和使用其他机制创建的名称之间只有一个小差异。 由于字符串是不可变的,并且保证了字符的唯一性,因此始终存在这样的可能性,即某个人在经历了字符串中所有可能的字符组合之后,将导致名称冲突。 从数学的角度来看,这意味着字符确实为我们提供了字符串所没有的宝贵机会。
在Node.js中,当检查对象时(例如,使用
console.log()
),如果检测到称为
inspect
的对象方法,则该方法用于获取对象的字符串表示形式,然后将其显示在屏幕上。 很容易理解,绝对每个人都不能考虑到这一点,因此系统的这种行为可以导致调用
inspect
对象方法,该方法旨在解决与对象的字符串表示形式无关的问题。 Node.js 10中不推荐使用此功能,在版本11中,具有相似名称的方法将被忽略。 现在,要实现此功能,提供了
require('util').inspect.custom
。 这意味着,没有人能够通过创建一种称为
inspect
的对象方法来无意中破坏系统。
仿制私人财产
这是一种有趣的方法,可用于模拟对象的私有属性。 这种方法涉及使用另一个现代JavaScript功能-代理对象。 这样的对象充当其他对象的包装,使程序员可以干预使用这些对象执行的动作。
代理对象提供了许多方法来拦截对对象执行的操作。 我们对控制读取对象键操作的能力感兴趣。 在此我们将不详细介绍代理对象。 如果您有兴趣,请查看
此出版物。
我们可以使用代理来控制对象的哪些属性从外部可见。 在这种情况下,我们要创建一个代理,以隐藏我们知道的两个属性。 一个具有字符串名称
_favColor
,第二个由写入
favBook
变量的字符表示:
let proxy; { const favBook = Symbol('fav book'); const obj = { name: 'Thomas Hunter II', age: 32, _favColor: 'blue', [favBook]: 'Metro 2033', [Symbol('visible')]: 'foo' }; const handler = { ownKeys: (target) => { const reportedKeys = []; const actualKeys = Reflect.ownKeys(target); for (const key of actualKeys) { if (key === favBook || key === '_favColor') { continue; } reportedKeys.push(key); } return reportedKeys; } }; proxy = new Proxy(obj, handler); } console.log(Object.keys(proxy));
处理名称由字符串
_favColor
表示的属性并不困难:只需阅读源代码即可。 动态键(如我们在上面看到的uuid键)可以与蛮力匹配。 但是,如果不引用该符号,则无法从
proxy
对象访问
Metro 2033
的值。
应该注意的是,在Node.js中,有一项功能侵犯了代理对象的隐私。 语言本身不存在此功能,因此它与其他JS运行时(例如浏览器)无关。 事实是,如果您有权访问代理对象,则此功能允许您访问隐藏在代理对象后面的对象。 这是一个示例,该示例演示了绕过前面代码片段中所示机制的能力:
const [originalObject] = process .binding('util') .getProxyDetails(proxy); const allKeys = Reflect.ownKeys(originalObject); console.log(allKeys[3]);
现在,为了防止在Node.js的特定实例中使用此功能,您必须修改全局
Reflect
对象或
util
进程的绑定。 但是,这是另一项任务。 如果您有兴趣,请阅读
这篇有关保护基于JavaScript的API的文章。
总结
在本文中,我们讨论了
Symbol
数据类型,它为JavaScript开发人员提供了哪些功能,以及可以使用哪些现有语言机制来模拟这些功能。
亲爱的读者们! 您在JavaScript项目中使用符号吗?
