Recursos do uso do tipo de dados Symbol em JavaScript

As primitivas de caracteres são uma das inovações do padrão ES6, que trouxe alguns recursos valiosos para o JavaScript. Os símbolos representados pelo tipo de dados Symbol são especialmente úteis quando usados ​​como identificadores para propriedades do objeto. Em conexão com esse cenário de aplicação, surge a pergunta sobre o que eles podem, o que as linhas não podem.



No material, cuja tradução publicamos hoje, falaremos sobre o tipo de dados Symbol em JavaScript. Começaremos analisando alguns dos recursos JavaScript que você precisa navegar para lidar com símbolos.

Informações preliminares


De fato, no JavaScript, existem dois tipos de valores. O primeiro tipo - valores primitivos, o segundo - objeto (eles também incluem funções). Os valores primitivos incluem tipos de dados simples como números (isso inclui tudo, desde números inteiros a números de ponto flutuante, valores Infinity e NaN ), valores lógicos, cadeias de caracteres, valores undefined e null . Observe que, ao verificar typeof null === 'object' retorna true , null é um valor primitivo.

Valores primitivos são imutáveis. Eles não podem ser alterados. Obviamente, você pode escrever algo novo em uma variável que armazena um valor primitivo. Por exemplo, isso grava um novo valor na variável x :

 let x = 1; x++; 

Mas, ao mesmo tempo, não há alteração (mutação) do valor numérico primitivo 1 .

Em algumas linguagens, por exemplo, em C, existem conceitos de passar argumentos de funções por referência e por valor. JavaScript também tem algo semelhante. Como exatamente o trabalho com dados é organizado depende de seu tipo. Se um valor primitivo representado por uma determinada variável for passado para a função e, em seguida, for alterado nessa função, o valor armazenado na variável original não será alterado. No entanto, se você passar o valor do objeto representado pela variável para a função e modificá-lo, o que é armazenado nessa variável também será alterado.

Considere o seguinte exemplo:

 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 

Valores primitivos (com exceção do misterioso NaN , que não é igual a si mesmo) sempre acabam sendo iguais a outros valores primitivos que se parecem com eles. Por exemplo:

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

No entanto, a construção de valores de objetos com a mesma aparência externa não levará ao fato de que as entidades serão obtidas, quando comparadas, sua igualdade entre si será revelada. Você pode verificar isso:

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

Os objetos desempenham um papel fundamental no JavaScript. Eles são usados ​​literalmente em todos os lugares. Por exemplo, eles são frequentemente usados ​​na forma de coleções de chave / valor. Porém, antes do advento do tipo de dados Symbol , apenas cadeias de caracteres podiam ser usadas como chaves de objeto. Essa foi uma limitação séria no uso de objetos na forma de coleções. Ao tentar atribuir um valor não-string como uma chave de objeto, esse valor foi convertido em uma string. Você pode verificar isso:

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

A propósito, embora isso nos afaste um pouco do tópico dos caracteres, gostaria de observar que a estrutura de dados do Map foi criada para permitir o uso de armazenamentos de dados de chave / valor em situações em que a chave não é uma string.

O que é um símbolo?


Agora que descobrimos os recursos dos valores primitivos no JavaScript, finalmente estamos prontos para começar a falar sobre caracteres. Um símbolo é um significado primitivo único. Se você se aproximar dos símbolos a partir dessa posição, notará que os símbolos a esse respeito são semelhantes aos objetos, pois a criação de várias instâncias dos símbolos levará à criação de valores diferentes. Além disso, os símbolos são valores primitivos imutáveis. Aqui está um exemplo de trabalho com caracteres:

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

Ao criar uma instância de um caractere, você pode usar o argumento opcional da primeira string. Este argumento é uma descrição do símbolo que se destina ao uso na depuração. Este valor não afeta o próprio símbolo.

 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) 

Símbolos como chaves para propriedade de objetos


Os símbolos podem ser usados ​​como chaves de propriedade para objetos. Isso é muito importante. Aqui está um exemplo de usá-los como tal:

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

Observe que as chaves especificadas por caracteres não são retornadas quando o método Object.keys() é Object.keys() . O código escrito antes da aparência dos caracteres em JS não sabe nada sobre eles; como resultado, as informações sobre as chaves dos objetos representados por caracteres não devem ser retornadas pelo método Object.keys() antigo.

À primeira vista, pode parecer que os recursos de caracteres acima permitem que você os use para criar propriedades particulares de objetos JS. Em muitas outras linguagens de programação, você pode criar propriedades de objetos ocultos usando classes. A falta desse recurso é considerada uma das deficiências do JavaScript.

Infelizmente, o código que funciona com objetos pode acessar livremente suas chaves de seqüência de caracteres. Além disso, o código pode acessar chaves especificadas por caracteres, mesmo se o código do qual eles trabalham com o objeto não tiver acesso ao caractere correspondente. Por exemplo, usando o método Reflect.ownKeys() , você pode obter uma lista de todas as chaves de um objeto, tanto as que são seqüências de caracteres quanto as que são os 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 

Observe que atualmente está em andamento o trabalho para equipar as classes com a capacidade de usar propriedades particulares. Esse recurso é chamado de campos particulares . É verdade que não afeta absolutamente todos os objetos, referindo-se apenas àqueles criados com base em classes previamente preparadas. O suporte para campos particulares já está disponível no navegador Chrome versão 72 e anterior.

Impedir colisões de nomes de propriedades de objetos


Os símbolos, é claro, não acrescentam ao JavaScript a capacidade de criar propriedades privadas de objetos, mas são uma inovação valiosa na linguagem por outros motivos. Ou seja, são úteis em situações em que determinadas bibliotecas precisam adicionar propriedades a objetos descritos fora delas e, ao mesmo tempo, não ter medo de uma colisão dos nomes das propriedades dos objetos.

Considere um exemplo no qual duas bibliotecas diferentes desejam adicionar metadados a um objeto. É possível que ambas as bibliotecas precisem equipar o objeto com alguns identificadores. Se você simplesmente usar algo como uma cadeia de caracteres de id de duas letras para o nome dessa propriedade, poderá encontrar uma situação em que uma biblioteca substitua a propriedade especificada pela outra.

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

Se usarmos os símbolos em nosso exemplo, cada biblioteca poderá gerar, na inicialização, os símbolos necessários. Esses símbolos podem ser usados ​​para atribuir propriedades a objetos e para acessar essas propriedades.

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

É analisando esse cenário que você pode se beneficiar da aparência de caracteres em JavaScript.

No entanto, pode haver uma pergunta sobre o uso de bibliotecas para os nomes de propriedades de objetos, cadeias aleatórias ou cadeias com uma estrutura complexa, incluindo, por exemplo, o nome da biblioteca. Sequências semelhantes podem formar algo como namespaces para identificadores usados ​​por bibliotecas. Por exemplo, pode ser assim:

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

Em geral, você pode fazê-lo. Abordagens semelhantes, de fato, são muito semelhantes ao que acontece ao usar símbolos. E se, usando identificadores aleatórios ou espaços para nome, algumas bibliotecas não gerarem, por acaso, os mesmos nomes de propriedade, não haverá problemas com os nomes.

Um leitor astuto diria agora que as duas abordagens consideradas para nomear propriedades de objetos não são completamente equivalentes. Os nomes de propriedades gerados aleatoriamente ou usando espaços para nome têm uma desvantagem: as chaves correspondentes são muito fáceis de encontrar, especialmente se o código pesquisar nas chaves de objetos ou serializá-las. Considere o seguinte exemplo:

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

Se um símbolo fosse usado para o nome da chave nessa situação, a representação JSON do objeto não conteria o valor do símbolo. Por que isso é assim? O fato é que o fato de um novo tipo de dados ter aparecido em JavaScript não significa que foram feitas alterações na especificação JSON. O JSON suporta, como chaves de propriedade, apenas cadeias. Ao serializar um objeto, nenhuma tentativa é feita para representar os caracteres de qualquer maneira especial.

O problema considerado de obter nomes de propriedades na representação JSON de objetos pode ser resolvido usando 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 

As chaves de seqüência de caracteres que são "ocultas" ao definir seu descritor enumerable como false se comportam da mesma maneira que as chaves representadas por caracteres. Ambos não são exibidos quando Object.keys() chamado e ambos podem ser detectados usando Reflect.ownKeys() . Aqui está o que parece:

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

Aqui, devo dizer, quase recriamos as possibilidades de símbolos, usando outros meios de JS. Em particular, ambas as chaves representadas por símbolos e chaves privadas não se enquadram na representação JSON de um objeto. Ambos podem ser encontrados consultando o método Reflect.ownKeys() . Como resultado, ambos não podem ser chamados de verdadeiramente privados. Se assumirmos que alguns valores aleatórios ou espaços de nomes de bibliotecas são usados ​​para gerar nomes-chave, isso significa que nos livramos do risco de colisões de nomes.

No entanto, há uma pequena diferença entre o uso de nomes de símbolos e nomes criados usando outros mecanismos. Como as strings são imutáveis ​​e os caracteres são garantidos únicos, sempre há a possibilidade de que alguém, depois de passar por todas as combinações possíveis de caracteres em uma string, cause uma colisão de nomes. Do ponto de vista matemático, isso significa que os personagens realmente nos dão uma oportunidade valiosa que as strings não têm.

No Node.js, ao examinar objetos (por exemplo, usando console.log() ), se um método de objeto chamado inspect detectado, esse método será usado para obter uma representação em cadeia do objeto e exibi-la na tela. É fácil entender que absolutamente todo mundo não pode levar isso em consideração; portanto, esse comportamento do sistema pode levar a uma chamada para o método de inspect objeto, que é projetado para resolver problemas que não estão relacionados à formação da representação de seqüência de caracteres do objeto. Esse recurso foi descontinuado no Node.js. 10, na versão 11, métodos com um nome semelhante são simplesmente ignorados. Agora, para implementar esse recurso, require('util').inspect.custom . Isso significa que ninguém jamais poderá interromper inadvertidamente o sistema criando um método de objeto chamado inspect .

Imitação de propriedades privadas


Aqui está uma abordagem interessante que você pode usar para simular as propriedades particulares dos objetos. Essa abordagem envolve o uso de outro recurso JavaScript moderno - objetos proxy. Esses objetos servem como invólucros para outros objetos que permitem ao programador intervir nas ações executadas com esses objetos.

Os objetos proxy oferecem várias maneiras de interceptar as ações executadas nos objetos. Estamos interessados ​​na capacidade de controlar a operação de leitura de chaves de um objeto. Não entraremos em detalhes sobre objetos proxy aqui. Se você estiver interessado, dê uma olhada nesta publicação.

Podemos usar proxies para controlar quais propriedades do objeto são visíveis do lado de fora. Nesse caso, queremos criar um proxy que oculte duas propriedades que conhecemos. Um deles tem o nome da string _favColor e o segundo é representado por um caractere gravado na variável 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 

Lidar com uma propriedade cujo nome é representado pela string _favColor não é difícil: basta ler o código-fonte. Teclas dinâmicas (como as teclas uuid que vimos acima) podem ser selecionadas com força bruta. Mas sem referência ao símbolo, você não pode acessar o valor do Metro 2033 partir do objeto proxy .

Note-se que no Node.js há um recurso que viola a privacidade dos objetos proxy. Esse recurso não existe no próprio idioma, portanto, não é relevante para outros tempos de execução do JS, como um navegador. O fato é que esse recurso permite acessar o objeto oculto atrás do objeto proxy, se você tiver acesso ao objeto proxy. Aqui está um exemplo que demonstra a capacidade de ignorar os mecanismos mostrados no snippet de código anterior:

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

Agora, para impedir o uso desse recurso em uma instância específica do Node.js., você deve modificar o objeto Reflect global ou a ligação do processo util . No entanto, essa é outra tarefa. Se você estiver interessado, dê uma olhada nesta postagem sobre como proteger APIs baseadas em JavaScript.

Sumário


Neste artigo, falamos sobre o tipo de dados Symbol , sobre quais recursos ele oferece aos desenvolvedores de JavaScript e sobre quais mecanismos de linguagem existentes podem ser usados ​​para simular esses recursos.

Caros leitores! Você usa símbolos em seus projetos JavaScript?

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


All Articles