在JavaScript中使用Symbol数据类型的功能

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



在我们今天发布的翻译材料中,我们将讨论JavaScript中的Symbol数据类型。 我们将首先回顾一些您需要导航以处理符号的JavaScript功能。

初步资料


实际上,在JavaScript中,有两种值。 第一种是原始值,第二种是对象(它们还包含函数)。 基本值包括简单的数据类型,例如数字(包括从整数到浮点数, InfinityNaN值的所有内容),逻辑值,字符串, undefinednull值。 请注意,在检查typeof null === 'object'产生truenull是原始值。

原始值是不可变的。 它们无法更改。 当然,您可以在存储原始值的变量中编写新内容。 例如,这将新值写入变量x

 let x = 1; x++; 

但是同时,原始数值1没有变化(变异)。

在某些语言中,例如在C语言中,存在通过引用和值传递函数自变量的概念。 JavaScript也有类似的东西。 数据工作的确切组织方式取决于其类型。 如果将某个变量表示的原始值传递给该函数,然后在该函数中对其进行更改,则存储在原始变量中的值不会更改。 但是,如果将变量代表的对象值传递给函数并对其进行修改,则此变量中存储的内容也会更改。

考虑以下示例:

 function primitiveMutator(val) { val = val + 1; } let x = 1; primitiveMutator(x); console.log(x); // 1 function objectMutator(val) { val.prop = val.prop + 1; } let obj = { prop: 1 }; objectMutator(obj); console.log(obj.prop); // 2 

原始值(神秘的NaN除外,它不等于自身)总是与其他看起来像它们自己的原始值相等。 例如:

 const first = "abc" + "def"; const second = "ab" + "cd" + "ef"; console.log(first === second); // true 

但是,从外部看相同的对象值的构造不会导致获得实体的事实,当比较时,将揭示它们之间的相等性。 您可以通过以下方式对此进行验证:

 const obj1 = { name: "Intrinsic" }; const obj2 = { name: "Intrinsic" }; console.log(obj1 === obj2); // false //     .name   : console.log(obj1.name === obj2.name); // true 

对象在JavaScript中起着基本作用。 它们几乎在任何地方都可以使用。 例如,它们通常以键/值集合的形式使用。 但是在Symbol数据类型出现之前,只能将字符串用作对象键。 这是对使用集合形式的对象的严重限制。 尝试将非字符串值分配为对象键时,此值已强制转换为字符串。 您可以通过以下方式对此进行验证:

 const obj = {}; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[{}] = 'someobj'; console.log(obj); // { '2': 2, foo: 'foo', bar: 'bar',    '[object Object]': 'someobj' } 

顺便说一句,尽管这使我们与字符的主题有些距离,但我想指出,创建Map数据结构是为了在键不是字符串的情况下允许使用键/值数据存储。

什么是符号?


现在我们已经弄清了JavaScript中原始值的功能,我们终于可以开始讨论字符了。 符号是唯一的原始含义。 如果从该位置接近符号,则将注意到这方面的符号与对象相似,因为创建符号的多个实例将导致创建不同的值。 但是,符号是不可变的原始值。 这是使用字符的示例:

 const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2); // false 

创建字符实例时,可以使用可选的第一个字符串参数。 此自变量是旨在用于调试的符号的描述。 该值不影响符号本身。

 const s1 = Symbol('debug'); const str = 'debug'; const s2 = Symbol('xxyy'); console.log(s1 === str); // false console.log(s1 === s2); // false console.log(s1); // Symbol(debug) 

符号作为对象属性的键


符号可以用作对象的属性键。 这很重要。 这是这样使用它们的示例:

 const obj = {}; const sym = Symbol(); obj[sym] = 'foo'; obj.bar = 'bar'; console.log(obj); // { bar: 'bar' } console.log(sym in obj); // true console.log(obj[sym]); // foo console.log(Object.keys(obj)); // ['bar'] 

请注意, 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));       // [ 'prop', Symbol(Pseudo Private) ] console.log(obj[Reflect.ownKeys(obj)[1]]); // 42 

请注意,目前正在努力使类具有使用私有属性的能力。 此功能称为“ 专用字段” 。 的确,它并不会绝对影响所有对象,而只是引用基于先前准备的类创建的对象。 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(); //       function lib1tag(obj) { obj[library1property] = 42; } const library2property = 'LIB2-NAMESPACE-id'; //     function lib2tag(obj) { obj[library2property] = 369; } 

通常,您可以这样做。 实际上,类似的方法与使用符号时的情况非常相似。 而且,如果使用随机标识符或名称空间,几个库不会偶然生成相同的属性名称,那么这些名称就不会有问题。

精明的读者现在会说,正在考虑的两种命名对象属性的方法并不完全等效。 随机生成或使用命名空间生成的属性名称有一个缺点:对应的键很容易找到,尤其是当代码搜索对象的键或将其序列化时。 考虑以下示例:

 const library2property = 'LIB2-NAMESPACE-id'; //    function lib2tag(obj) { obj[library2property] = 369; } const user = { name: 'Thomas Hunter II', age: 32 }; lib2tag(user); JSON.stringify(user); // '{"name":"Thomas Hunter II","age":32,"LIB2-NAMESPACE-id":369}' 

如果在这种情况下将符号用作键名,则对象的JSON表示将不包含符号值。 为什么会这样呢? 事实是,JavaScript中出现了新的数据类型,并不意味着已经对JSON规范进行了更改。 JSON仅支持字符串作为属性键。 序列化对象时,不会尝试以任何特殊方式表示字符。

通过使用Object.defineProperty()可以解决在对象的JSON表示形式中获取属性名称的问题:

 const library2property = uuid(); //   function lib2tag(obj) { Object.defineProperty(obj, library2property, {   enumerable: false,   value: 369 }); } const user = { name: 'Thomas Hunter II', age: 32 }; lib2tag(user); // '{"name":"Thomas Hunter II",  "age":32,"f468c902-26ed-4b2e-81d6-5775ae7eec5d":369}' console.log(JSON.stringify(user)); console.log(user[library2property]); // 369 

通过将其enumerable 描述符设置为false来“隐藏”的字符串键的行为与字符表示的键几乎相同。 当调用Object.keys()时,它们都不显示,并且都可以使用Reflect.ownKeys()来检测。 看起来是这样的:

 const obj = {}; obj[Symbol()] = 1; Object.defineProperty(obj, 'foo', { enumberable: false, value: 2 }); console.log(Object.keys(obj)); // [] console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ] console.log(JSON.stringify(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)); // [ 'name', 'age' ] console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ] console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ] console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)] console.log(proxy._favColor); // 'blue 

处理名称由字符串_favColor表示的属性并不困难:只需阅读源代码即可。 动态键(如我们在上面看到的uuid键)可以与蛮力匹配。 但是,如果不引用该符号,则无法从proxy对象访问Metro 2033的值。

应该注意的是,在Node.js中,有一项功能侵犯了代理对象的隐私。 语言本身不存在此功能,因此它与其他JS运行时(例如浏览器)无关。 事实是,如果您有权访问代理对象,则此功能允许您访问隐藏在代理对象后面的对象。 这是一个示例,该示例演示了绕过前面代码片段中所示机制的能力:

 const [originalObject] = process .binding('util') .getProxyDetails(proxy); const allKeys = Reflect.ownKeys(originalObject); console.log(allKeys[3]); // Symbol(fav book) 

现在,为了防止在Node.js的特定实例中使用此功能,您必须修改全局Reflect对象或util进程的绑定。 但是,这是另一项任务。 如果您有兴趣,请阅读这篇有关保护基于JavaScript的API的文章。

总结


在本文中,我们讨论了Symbol数据类型,它为JavaScript开发人员提供了哪些功能,以及可以使用哪些现有语言机制来模拟这些功能。

亲爱的读者们! 您在JavaScript项目中使用符号吗?

Source: https://habr.com/ru/post/zh-CN444340/


All Articles