Características del uso del tipo de datos Symbol en JavaScript

Las primitivas de caracteres son una de las innovaciones del estándar ES6, que trajo algunas características valiosas a JavaScript. Los símbolos representados por el tipo de datos Símbolo son especialmente útiles cuando se usan como identificadores para las propiedades del objeto. En relación con tal escenario de su aplicación, surge la pregunta de qué pueden y qué no pueden hacer las líneas.



En el material, cuya traducción publicamos hoy, hablaremos sobre el tipo de datos Symbol en JavaScript. Comenzaremos revisando algunas de las funciones de JavaScript que necesita navegar para manejar los símbolos.

Información preliminar


En JavaScript, de hecho, hay dos tipos de valores. El primer tipo - valores primitivos, el segundo - objeto (también incluyen funciones). Los valores primitivos incluyen tipos de datos simples como números (esto incluye todo, desde números enteros hasta números de coma flotante, valores Infinity y NaN ), valores lógicos, cadenas, valores undefined y null . Tenga en cuenta que mientras se verifica typeof null === 'object' produce true , null es un valor primitivo.

Los valores primitivos son inmutables. No pueden ser cambiados. Por supuesto, puede escribir algo nuevo en una variable que almacene un valor primitivo. Por ejemplo, esto escribe un nuevo valor en la variable x :

 let x = 1; x++; 

Pero al mismo tiempo, no hay cambio (mutación) del valor numérico primitivo 1 .

En algunos lenguajes, por ejemplo, en C, hay conceptos de pasar argumentos de funciones por referencia y por valor. JavaScript también tiene algo similar. Cómo se organiza exactamente el trabajo con datos depende de su tipo. Si un valor primitivo representado por una determinada variable se pasa a la función, y luego se cambia en esta función, el valor almacenado en la variable original no cambia. Sin embargo, si pasa el valor del objeto representado por la variable a la función y lo modifica, lo que está almacenado en esta variable también cambiará.

Considere el siguiente ejemplo:

 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 

Los valores primitivos (con la excepción del misterioso NaN , que no es igual a sí mismo) siempre resultan ser iguales a otros valores primitivos que se parecen a ellos mismos. Por ejemplo:

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

Sin embargo, la construcción de valores de objeto que se vean igual hacia afuera no conducirá al hecho de que se obtendrán entidades, en comparación, se revelará su igualdad entre sí. Puede verificar esto mediante:

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

Los objetos juegan un papel fundamental en JavaScript. Se usan literalmente en todas partes. Por ejemplo, a menudo se usan en forma de colecciones de clave / valor. Pero antes del advenimiento del tipo de datos Symbol , solo se podían usar cadenas como claves de objeto. Esta fue una limitación seria en el uso de objetos en forma de colecciones. Al intentar asignar un valor que no sea de cadena como una clave de objeto, este valor se convirtió en una cadena. Puede verificar esto mediante:

 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' } 

Por cierto, aunque esto nos aleja un poco del tema de los caracteres, quiero señalar que la estructura de datos del Map se creó para permitir el uso de almacenes de datos de clave / valor en situaciones donde la clave no es una cadena.

¿Qué es un símbolo?


Ahora que hemos descubierto las características de los valores primitivos en JavaScript, finalmente estamos listos para comenzar a hablar sobre los personajes. Un símbolo es un significado primitivo único. Si se acerca a los símbolos desde esta posición, notará que los símbolos a este respecto son similares a los objetos, ya que la creación de varias instancias de los símbolos conducirá a la creación de diferentes valores. Pero los símbolos, además, son valores primitivos inmutables. Aquí hay un ejemplo de trabajo con personajes:

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

Al crear una instancia de un personaje, puede usar el primer argumento de cadena opcional. Este argumento es una descripción del símbolo que está destinado a usarse en la depuración. Este valor no afecta el símbolo en sí.

 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) 

Los símbolos como claves de propiedad de los objetos.


Los símbolos se pueden usar como claves de propiedad para objetos. Esto es muy importante Aquí hay un ejemplo de cómo usarlos como tales:

 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'] 

Tenga en cuenta que las claves especificadas por los caracteres no se devuelven cuando se Object.keys() método Object.keys() . El código escrito antes de la aparición de los caracteres en JS no sabe nada sobre ellos, como resultado, la información sobre las claves de los objetos representados por los caracteres no debe ser devuelta por el antiguo método Object.keys() .

A primera vista, puede parecer que las características anteriores de los caracteres le permiten usarlas para crear propiedades privadas de objetos JS. En muchos otros lenguajes de programación, puede crear propiedades de objetos ocultos utilizando clases. La falta de esta característica ha sido considerada una de las deficiencias de JavaScript.

Desafortunadamente, el código que funciona con objetos puede acceder libremente a sus claves de cadena. El código también puede acceder a las teclas especificadas por los caracteres, además, incluso si el código desde el que trabajan con el objeto no tiene acceso al carácter correspondiente. Por ejemplo, utilizando el método Reflect.ownKeys() , puede obtener una lista de todas las claves de un objeto, tanto las que son cadenas como las que son caracteres:

 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 

Tenga en cuenta que actualmente se está trabajando para equipar a las clases con la capacidad de usar propiedades privadas. Esta característica se llama Campos privados . Es cierto que no afecta absolutamente a todos los objetos, sino que se refiere solo a aquellos creados a partir de clases preparadas previamente. El soporte para campos privados ya está disponible en el navegador Chrome versión 72 y anteriores.

Prevenir colisiones de nombres de propiedades de objetos


Los símbolos, por supuesto, no agregan a JavaScript la capacidad de crear propiedades privadas de objetos, pero son una innovación valiosa en el lenguaje por otras razones. Es decir, son útiles en situaciones en las que ciertas bibliotecas necesitan agregar propiedades a los objetos descritos fuera de ellas y, al mismo tiempo, no temer una colisión de los nombres de las propiedades de los objetos.

Considere un ejemplo en el que dos bibliotecas diferentes desean agregar metadatos a un objeto. Es posible que ambas bibliotecas necesiten equipar el objeto con algunos identificadores. Si simplemente usa algo como una cadena de id de dos letras para el nombre de dicha propiedad, puede encontrar una situación en la que una biblioteca sobrescribe la propiedad especificada por la otra.

 function lib1tag(obj) { obj.id = 42; } function lib2tag(obj) { obj.id = 369; } 

Si usamos los símbolos en nuestro ejemplo, cada biblioteca puede generar, tras la inicialización, los símbolos que necesita. Estos símbolos se pueden usar para asignar propiedades a los objetos y acceder a estas propiedades.

 const library1property = Symbol('lib1'); function lib1tag(obj) { obj[library1property] = 42; } const library2property = Symbol('lib2'); function lib2tag(obj) { obj[library2property] = 369; } 

Al observar este escenario, puede beneficiarse de la aparición de caracteres en JavaScript.

Sin embargo, puede haber una pregunta con respecto al uso de bibliotecas para los nombres de propiedades de objetos, cadenas aleatorias o cadenas con una estructura compleja, que incluye, por ejemplo, el nombre de la biblioteca. Cadenas similares pueden formar algo así como espacios de nombres para los identificadores utilizados por las bibliotecas. Por ejemplo, podría verse así:

 const library1property = uuid(); //       function lib1tag(obj) { obj[library1property] = 42; } const library2property = 'LIB2-NAMESPACE-id'; //     function lib2tag(obj) { obj[library2property] = 369; } 

En general, puedes hacerlo. Enfoques similares, de hecho, son muy similares a lo que sucede cuando se usan símbolos. Y si, utilizando identificadores aleatorios o espacios de nombres, un par de bibliotecas no generarán, por casualidad, los mismos nombres de propiedad, entonces no habrá problemas con los nombres.

Un lector astuto diría ahora que los dos enfoques considerados para nombrar propiedades de objeto no son completamente equivalentes. Los nombres de propiedades que se generan aleatoriamente o que usan espacios de nombres tienen un inconveniente: las claves correspondientes son muy fáciles de encontrar, especialmente si el código busca las claves de los objetos o las serializa. Considere el siguiente ejemplo:

 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}' 

Si se utilizara un símbolo para el nombre de la clave en esta situación, la representación JSON del objeto no contendría el valor del símbolo. ¿Por qué es esto así? El hecho es que el hecho de que haya aparecido un nuevo tipo de datos en JavaScript no significa que se hayan realizado cambios en la especificación JSON. JSON admite, como claves de propiedad, solo cadenas. Al serializar un objeto, no se intenta representar a los personajes de ninguna manera especial.

El problema considerado de obtener nombres de propiedad en la representación JSON de objetos se puede resolver utilizando Object.defineProperty() :

 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 

Las teclas de cadena que están "ocultas" configurando su descriptor enumerable en false comportan de la misma manera que las teclas representadas por caracteres. Ambos no se muestran cuando Object.keys() llama Object.keys() , y ambos se pueden detectar usando Reflect.ownKeys() . Así es como se ve:

 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)); // {} 

Aquí, debo decir, casi recreamos las posibilidades de los símbolos, utilizando otros medios de JS. En particular, ambas claves representadas por símbolos y claves privadas no entran en la representación JSON de un objeto. Ambos pueden reconocerse haciendo referencia al método Reflect.ownKeys() . Como resultado, a ambos no se les puede llamar verdaderamente privados. Si suponemos que se utilizan algunos valores aleatorios o espacios de nombres de biblioteca para generar nombres clave, entonces esto significa que nos libramos del riesgo de colisiones de nombres.

Sin embargo, hay una pequeña diferencia entre usar nombres de símbolos y nombres creados usando otros mecanismos. Dado que las cadenas son inmutables y se garantiza que los caracteres serán únicos, siempre existe la posibilidad de que alguien, después de pasar por todas las combinaciones posibles de caracteres en una cadena, cause una colisión de nombres. Desde un punto de vista matemático, esto significa que los personajes realmente nos dan una valiosa oportunidad que las cadenas no tienen.

En Node.js, al examinar objetos (por ejemplo, usando console.log() ), si inspect detecta un método de objeto llamado inspect , este método se utiliza para obtener una representación de cadena del objeto y luego mostrarlo en la pantalla. Es fácil entender que absolutamente todos no pueden tener esto en cuenta, por lo tanto, tal comportamiento del sistema puede conducir a una llamada al método de inspect objetos, que está diseñado para resolver problemas que no están relacionados con la formación de la representación de cadena del objeto. Esta característica está en desuso en Node.js 10, en la versión 11, los métodos con un nombre similar simplemente se ignoran. Ahora, para implementar esta característica, require('util').inspect.custom . Esto significa que nadie podrá interrumpir inadvertidamente el sistema creando un método de objeto llamado inspect .

Imitación de propiedades privadas.


Aquí hay un enfoque interesante que puede usar para simular las propiedades privadas de los objetos. Este enfoque implica el uso de otra característica moderna de JavaScript: los objetos proxy. Dichos objetos sirven como envoltorios para otros objetos que permiten al programador intervenir en las acciones realizadas con estos objetos.

Los objetos proxy ofrecen muchas formas de interceptar las acciones realizadas en los objetos. Estamos interesados ​​en la capacidad de controlar la operación de lectura de claves de un objeto. No entraremos en detalles sobre los objetos proxy aquí. Si está interesado, eche un vistazo a esta publicación.

Podemos usar proxies para controlar qué propiedades del objeto son visibles desde el exterior. En este caso, queremos crear un proxy que oculte dos propiedades que conocemos. Uno tiene el nombre de cadena _favColor , y el segundo está representado por un carácter escrito en la variable 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 

Tratar con una propiedad cuyo nombre está representado por la cadena _favColor no es difícil: solo lea el código fuente. Las teclas dinámicas (como las teclas uuid que vimos anteriormente) se pueden combinar con la fuerza bruta. Pero sin referencia al símbolo, no puede acceder al valor de Metro 2033 desde el objeto proxy .

Cabe señalar que en Node.js hay una característica que viola la privacidad de los objetos proxy. Esta característica no existe en el lenguaje en sí, por lo que no es relevante para otros tiempos de ejecución de JS, como un navegador. El hecho es que esta característica le permite acceder al objeto oculto detrás del objeto proxy, si tiene acceso al objeto proxy. Aquí hay un ejemplo que demuestra la capacidad de eludir los mecanismos que se muestran en el fragmento de código anterior:

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

Ahora, para evitar el uso de esta función en una instancia específica de Node.js, debe modificar el objeto Reflect global o el enlace del proceso util . Sin embargo, esta es otra tarea. Si está interesado, eche un vistazo a esta publicación sobre la protección de las API basadas en JavaScript.

Resumen


En este artículo, hablamos sobre el tipo de datos de Symbol , sobre las características que proporciona a los desarrolladores de JavaScript y sobre qué mecanismos de lenguaje existentes se pueden usar para simular estas características.

Estimados lectores! ¿Utiliza símbolos en sus proyectos de JavaScript?

Source: https://habr.com/ru/post/444340/


All Articles