Ao trabalhar com valores e objetos em JavaScript, às vezes você pode precisar restringir o que pode ser feito com eles para evitar alterações em objetos de configuração de todo o aplicativo, objetos de estado, ou constantes globais.

Funções com acesso a tais dados podem modificá-los diretamente quando não deveriam (e isso também pode resultar de erros não intencionais cometidos por desenvolvedores). Além disso, outros desenvolvedores trabalhando na mesma base de código (ou usando o seu código) podem fazer essas alterações inesperadamente.

Felizmente, o JavaScript fornece algumas construções para lidar com esses tipos de situações.

Neste No tutorial, discutiremos o conceito de imutabilidade e os métodos de objeto freeze () e seal () em JavaScript. Veremos como eles funcionam usando exemplos de código ilustrativos e discutiremos as possíveis limitações de desempenho. Agora, vamos ao que interessa!

Compreendendo a imutabilidade em JavaScript

Em resumo, tornar um objeto imutável significa que outras alterações nele não se aplicarão. Essencialmente, seu estado se torna somente leitura. Isso é, até certo ponto, o que a palavra-chave const consegue:

const jarOfWine=”full”;//lança o erro”TypeError não capturado: atribuição à variável constante.”jarOfWine=”vazio”;

Mas é claro, não podemos usar const para entidades como objetos e arrays por causa de como as declarações const funcionam-ele simplesmente cria uma referência a um valor. Para explicar isso, vamos revisar os tipos de dados JavaScript.

Primitivos vs. objetos

O primeiro conjunto de tipos de dados são valores que consistem em apenas um item. Isso inclui primitivos como strings ou números que são imutáveis:

let nextGame=”Word Duel”;//mudar para”Word Dual”? Não gruda. jarOfWine [7]=”a”; próximo jogo;//ainda”Word Duel”//Claro, se tivéssemos declarado nextGame com `const`, então não poderíamos reatribuí-lo. nextGame=”Palavra Dual”; próximo jogo;//agora”Word Dual”

Quando copiamos esses tipos primitivos, estamos copiando os valores:

const jarOfWine=”full”; const emptyJar=jarOfWine;//os dois jars agora estão’full’

Ambas as variáveis, jarOfWine e emptyJar, agora contêm duas strings separadas e você pode alterar qualquer uma delas independentemente da outra. No entanto, os objetos se comportam de maneira diferente.

Quando você declara um objeto, como no código a seguir, a variável de usuário não contém o objeto em si, mas uma referência a ele:

const user={name:”Jane”, sobrenome:”Traveller”, stayDuration:”3 semanas”, roomAssigned: 1022,}

É como escrever o endereço da caverna que contém sua pilha de ouro. O endereço não é a caverna. Então, quando tentamos copiar um objeto usando o mesmo método de atribuição de quando copiamos strings, acabamos copiando apenas a referência ou endereço e não temos dois objetos separados:

const guest=user;

A modificação do usuário também altera o convidado:

guest.name=”John”;//agora, o usuário e o convidado têm a seguinte aparência: {name:”John”, sobrenome:”Traveller”, stayDuration:”3 semanas”, roomAssigned: 1022,}

Normalmente, você pode testar isso com Object.is ( ) ou o operador de igualdade estrita:

Object.is (usuário, convidado)//retorna usuário verdadeiro===convidado//retorna verdadeiro

É um jogo semelhante com a palavra-chave const. Ele cria uma referência a um valor, o que significa que embora a ligação não possa mudar (ou seja, você não pode reatribuir a variável), o valor referenciado pode mudar.

Isso ocorreu quando modificamos com sucesso a propriedade de nome anteriormente, mesmo que guest tenha sido declarado com const:

guest.name=”John”;

Em outras palavras, o que const nos dá é a imutabilidade de atribuição, não a imutabilidade de valor.

Restringindo mudanças nas propriedades do objeto e objetos inteiros

Visto que os objetos em JavaScript são copiados por referência, sempre há o risco de que as referências copiadas alterem o objeto original. Dependendo do seu caso de uso, esse comportamento pode não ser desejável. Nesse caso, pode fazer sentido essencialmente “bloquear” o objeto.

(Idealmente, você faria cópias do seu objeto e as modificaria, em vez do objeto original. Enquanto a maioria copia ou clona mecanismos são superficiais, se você estiver trabalhando com objetos profundamente aninhados, então você desejará clonagem profunda .)

JavaScript fornece três métodos que executam vários níveis de restrição de acesso a objetos. Isso inclui Object.freeze (), Object.seal () e Object.preventExtensions (). Embora cobriremos o último um pouco, vamos nos concentrar principalmente nos dois anteriores.

sinalizadores de propriedade graváveis ​​e configuráveis ​​

Antes de prosseguirmos, no entanto, vamos examinar alguns aspectos subjacentes conceitos por trás dos mecanismos que limitam o acesso às propriedades. Especificamente, estamos interessados ​​em sinalizadores de propriedade , como gravável e configurável.

Normalmente, você pode verificar o valores desses sinalizadores ao usar os métodos Object.getOwnPropertyDescriptor ou Object.getOwnPropertyDescriptors:

const hunanProvince={typeOfWine:”Emperor’s Smile”,}; Object.getOwnPropertyDescriptors (hunanProvince);//retorna {typeOfWine: {value:”Emperor’s Smile”, writable: true, enumerable: true, configurable: true},}

Embora geralmente estejamos mais preocupados com os valores reais de nossas propriedades quando trabalhamos com JavaScript os objetos, as propriedades possuem outros atributos além do atributo value, que contém o valor da propriedade.

Incluem os atributos já mencionados valor, gravável e configurável, bem como enumeráveis, conforme visto acima.

Os sinalizadores graváveis ​​e configuráveis ​​são os mais importantes para nós. Quando gravável é definido como verdadeiro para uma propriedade, seu valor pode mudar. Caso contrário, é somente leitura.

Então há configurável, que, quando definido como verdadeiro em uma propriedade, permite que você faça alterações nas sinalizações mencionadas ou exclua uma propriedade.

Se configurável em vez disso, é definido como falso, tudo se torna essencialmente somente leitura com uma exceção: se gravável é definido como verdadeiro onde configurável é falso, o valor da propriedade ainda pode mudar:

Object.defineProperty (hunanProvince,”capital”, {valor:”Caiyi Town”, gravável: true,}); hunanProvince.capital=”Possivelmente Gusu”; Object.getOwnPropertyDescriptors (hunanProvince);//agora retorna {typeOfWine: {value:”Emperor’s Smile”, writable: true, enumerable: true, configurable: true}, capital: {value:”Possibly Gusu”, writable: true, enumerable: false, configurable: false} ,}

Observe que enumerável e configurável são ambos falsos para a propriedade de capital aqui porque ela foi criada com Object.defineProperty () . Conforme mencionado anteriormente, as propriedades criadas dessa maneira têm todos os sinalizadores definidos como falsos. No entanto, gravável é verdadeiro porque definimos isso explicitamente.

Também podemos alterar gravável de verdadeiro para falso, mas é isso. Você não pode alterá-lo de falso para verdadeiro. Na verdade, uma vez que configurável e gravável são definidos como falsos para uma propriedade, nenhuma alteração adicional é permitida:

Object.defineProperty (hunanProvince,”capital”, {gravável: false,//todo o resto também `false `});//sem efeito hunanProvince.capital=”Caiyi Town”;

Embora esses sinalizadores sejam usados ​​aqui em um nível de propriedade, métodos como Object.freeze () e Object.seal () funcionam em um nível de objeto. Vamos ver isso agora.

Este artigo pressupõe que você tenha um conhecimento geral de por que o conceito de imutabilidade é útil.

No entanto, se quiser se aprofundar e ler alguns argumentos a favor e contra, aqui está um Tópico StackOverflow (com links para recursos adicionais) que discute o tópico. Os documentos Immutable.js também defendem a imutabilidade .

Usando o objeto.freeze vs. Object.seal para imutabilidade do objeto

Agora, vamos dar uma olhada nos métodos congelar e selar.

Usando Object.freeze

Quando nós congele um objeto usando Object.freeze, ele não pode mais ser modificado. Essencialmente, novas propriedades não podem mais ser adicionadas a ele e as propriedades existentes não podem ser removidas. Como você pode imaginar, isso é conseguido definindo todos os sinalizadores como falsos para todas as propriedades.

Vejamos um exemplo. Aqui estão os dois objetos com os quais trabalharemos:

let obj1={“um”: 1,”dois”: 2,}; deixe obj2={“três”: 3,”quatro”: 4,};

Agora, vamos mudar uma propriedade no primeiro objeto, obj1:

obj1.one=”one”;//retorna”um”

Então, o objeto original agora se parece com isto:

obj1; {um:”um”, dois: 2,};

Claro, esse é o comportamento esperado. Os objetos são alteráveis ​​por padrão. Agora, vamos tentar congelar um objeto. Vamos trabalhar com obj2, uma vez que ainda não foi adulterado:

//freeze () retorna o mesmo objeto passado para ele Object.freeze (obj2);//retorna {três: 3, quatro: 2}//teste obj2===Object.freeze (obj2);//retorna verdadeiro

Para testar se um objeto está congelado, JavaScript fornece o método Object.isFrozen ():

Object.isFrozen (obj2);//retorna verdadeiro

Agora, mesmo se tentarmos modificá-lo como a seguir, não haverá efeito.

obj2.three=”três”;//sem efeito

No entanto, como veremos em breve, teremos problemas quando começarmos a usar objetos aninhados. Como a clonagem de objetos, o congelamento também pode ser superficial ou profundo.

Vamos criar um novo objeto a partir de obj1 e obj2 e aninhar um array nele:

//nesting let obj3=Object.assign ({ }, obj1, obj2, {“otherNumbers”: {“even”: [6, 8, 10],”odd”: [5, 7, 9],}}); obj3;//{//um:”um”,//dois: 2,//três: 3,//quatro: 4,//”outrosNúmeros”: {//”par”: [6, 8, 10],//”ímpar”: [5, 7, 9],//}//}

Você perceberá que mesmo quando o congelarmos, ainda podemos fazer alterações nas matrizes no objeto aninhado:

Object.freeze (obj3); obj3.otherNumbers.even [0]=12; obj3;//{//um:”um”,//dois: 2,//três: 3,//quatro: 4,//”outrosNúmeros”: {//”par”: [12, 8, 10],//”ímpar”: [5, 7, 9],//}//}

A matriz de número par agora tem seu primeiro elemento modificado de 6 para 12. Como as matrizes também são objetos, esse comportamento surge aqui como bem:

deixe testArr=[0, 1, 2, 3, [4, 5, [6, 7]]]; Object.freeze (testArr); testArr [0]=”zero”;//incapaz de modificar os elementos de nível superior…//… entretanto, os elementos aninhados podem ser alterados testArr \ [4 \] [0]=”quatro”;//agora se parece com isto: [0, 1, 2, 3, [“quatro”, 5, [6, 7]]]

Se você testou seu código no console do navegador, provavelmente falhou silenciosamente e não lançou nenhum erro. Se desejar que os erros sejam mais explícitos, tente envolver seu código em uma Expressão de função imediatamente invocada (IIFE ) e ative o modo estrito:

(function () {“use strict”; let obj={“one”: 1,”two”: 2}; Object.freeze (obj); obj.um=”um”;}) ();

O código acima agora deve lançar um TypeError no console:

TypeError não capturado: Não é possível atribuir à propriedade somente leitura’um’do objeto’#

Agora, como fazemos nosso todo objeto, incluindo nível superior (referências diretas de propriedade) e propriedades aninhadas, congelado?

Como observamos, o congelamento é aplicado apenas às propriedades de nível superior em objetos, então um deepFreeze () função que congela cada propriedade recursivamente é o que queremos:

const deepFreeze=(obj)=> {//buscar chaves de propriedade const propKeys=Object.getOwnPropertyNames (obj);//congela recursivamente todas as propriedades propKeys.forEach ((key)=> {const propValue=obj [key]; if (propValue && typeof (propValue)===”objeto”) deepFreeze (propValue);}); return Object.freeze (obj); }

Agora, as tentativas de mutação das propriedades aninhadas não têm êxito.

Observe que, embora o congelamento essencialmente proteja contra alterações em objetos, ele permite a reatribuição de variáveis.

Usando Object.seal ()

Com Object.freeze (), novas alterações não têm efeito no objeto congelado. No entanto, o método seal () permite modificar as propriedades existentes. Isso significa que, embora você não possa adicionar novas propriedades ou remover as existentes, pode fazer alterações.

O método seal () basicamente define o sinalizador configurável que discutimos anteriormente como falso, com gravável definido como verdadeiro para cada propriedade:

const students={“001″:”Kylie Yaeger”,”002″:”Ifeoma Kurosaki”};//lacra o objeto Object.seal (alunos);//teste Object.isSealed (alunos);//retorna verdadeiro//não pode adicionar ou excluir propriedades students [“003″]=”Amara King”;//falha ao excluir alunos [“001”];//falha

Aqui está outro exemplo com uma matriz:

const students=[“Kylie Yaeger”,”Ifeoma Kurosaki”];//selo Object.seal (alunos);//teste Object.isSealed (alunos);//retorna verdadeiro//lança um TypeError dizendo que o objeto não é extensível students.push (“Amara King”);

A vedação também evita a redefinição de uma propriedade com o uso de Object.defineProperty () ou Object.defineProperties (), esteja você adicionando uma nova propriedade ou modificando uma existente.

Lembre-se, no entanto, que se gravável for verdadeiro, você ainda pode alterá-lo para falso, mas isso não pode ser desfeito.

//falha Object.defineProperty (hunanProvince,”capital”, {valor:”Desconhecido”, gravável: verdadeiro,}) ;

Outra alteração que o lacre torna impossível é alterar as propriedades normais dos dados em acessadores (ou seja, getters e setters):

//falha Object.defineProperty (hunanProvince,”capital”, {get: ()=>”Caiyi Cidade”, defina: (val)=> hunanProvince [“capital”]=val;});

O inverso também é o caso: você não pode transformar acessores em propriedades de dados. Assim como no congelamento, lacrar um objeto evita que seu protótipo mude:

const languageSymbols={English:”ENG”, Japanese:”JP”, French:”FR”,}; const trollLanguageSymbols={trollEnglish:”T-ENG”, trollJapanese:”T-JP”, trollFrench:”T-FR”,}; Object.seal (trollLanguageSymbols);//falha Object.setPrototypeOf (trollLanguageSymbols, languageSymbols);

Novamente, assim como no congelamento, o comportamento padrão aqui é a vedação superficial. Portanto, você pode optar por lacrar profundamente um objeto da mesma forma que congelar profundamente um:

const deepSeal=(obj)=> {//buscar chaves de propriedade const propKeys=Object.getOwnPropertyNames (obj) ;//selar recursivamente todas as propriedades propKeys.forEach ((key)=> {const propValue=obj [key]; if (propValue && typeof (propValue)===”objeto”) deepSeal (propValue);}); return Object.seal (obj); }

Modificamos a função deepFreeze () do MDN aqui para realizar a vedação:

const students={“001″:”Kylie Yaeger”,”002″:”Ifeoma Kurosaki”,”003″: {“004″:”Yumi Ren”,”005″:”Plisetsky Ran”,},}; deepSeal (alunos);//falha ao excluir alunos2 \ [“003″\] [“004”];

Agora, nossos objetos aninhados também são lacrados.

Usando Object.preventExtensions ()

Outro método JavaScript que pode prevenir especificamente a adição de novas propriedades é o método preventExtensions ():

(()=> {“use strict”; const trollToken={name:”Troll”, símbolo:”TRL”, decimal: 6, totalSupply: 100_000_000,}; Object.preventExtensions (trollToken);//falha trollToken.transfer=(_to, quantidade)=> {}}) ();

Já que tudo o que estamos fazendo é evitar a adição de novas propriedades, as existentes podem obviamente ser modificadas e até mesmo excluídas:

delete trollToken.decimal; trollToken;//{//nome:”Troll”,//símbolo:”TRL”,//totalSupply: 100_000_000,//}

Algo a se notar é que a propriedade [[protótipo]] se torna imutável:

símbolo const={transferência: ()=> {}, transferência de: ()=> {}, aprovação: ()=> {},};//falha com um TypeError Object.setPrototypeOf (trollToken, token);

Para testar se um objeto é extensível, basta usar o método isExtensible ():

//Omiti `console.log` aqui, pois presumo que você esteja digitando diretamente no console do navegador ( `O trollToken é extensível? Resp: $ {Object.isExtensible (trollToken)}`);

Assim como quando definimos manualmente os sinalizadores configuráveis ​​e graváveis ​​como falsos para uma propriedade, tornar um objeto inextensível é uma estrada de mão única.

Casos de uso e questões de desempenho de Object.freeze e Object.seal

Para resumir, Object.freeze () e Object.seal () são construções fornecidas pela linguagem JavaScript para ajudar a manter vários níveis de integridade para objetos. No entanto, pode ser bastante confuso entender quando é necessário usar esses métodos.

Um exemplo mencionado anteriormente é o uso de objetos globais para gerenciamento de estado do aplicativo. Você pode querer manter o objeto original imutável e fazer alterações nas cópias, especialmente se quiser acompanhar as alterações de estado e revertê-las.

O congelamento protege contra a tentativa de código de alterar objetos que não deveriam ser modificados diretamente.

Objetos congelados ou lacrados também podem impedir a adição de novas propriedades que são introduzidas devido a erros de digitação, como nomes de propriedade digitados incorretamente.

Esses métodos também ajudam na depuração porque as restrições colocadas em objetos podem ajudar a restringir possíveis fontes de bugs.

Dito isso, pode ser uma fonte de dor de cabeça para qualquer pessoa que use o seu código, uma vez que essencialmente não há diferença física entre um objeto congelado e um não-frozen one.

A única maneira de saber com certeza se um objeto está congelado ou lacrado é usar os métodos isFrozen () ou isSealed (). Isso pode dificultar um pouco o raciocínio sobre o comportamento esperado do objeto porque pode não ser totalmente óbvio por que tais restrições foram postas em prática.

Modelos marcados são um recurso que usa Object.freeze () implicitamente /nltbvsYnlvsYnlcttps://docs.google.com/document/d/1X6zO5F_Zojizn2dmo_ftaOwsYnltbeview/rplicativo ; a biblioteca de componentes estilizados e alguns outros dependem dela. O primeiro usa literais de modelo marcados para criar seus componentes estilizados .

Se você está se perguntando quais são-se houver-custos de desempenho ao usar qualquer um dos métodos discutidos acima, houve algumas preocupações históricas de desempenho no motor V8 . No entanto, isso era mais um bug do que qualquer outra coisa e já foi corrigido .

Entre 2013 e 2014 , ambos Object.freeze ( ) e Object.seal () também passaram por algumas melhorias de desempenho no V8.

Aqui está um thread StackOverflow que rastreou o desempenho de objetos congelados vs. objetos não congelados entre 2015 e 2019. Isso mostra que o desempenho em ambos os casos é praticamente o mesmo no Chrome.

Ainda assim, é possível selar ou congelar pode afetar a velocidade de enumeração de um objeto em certos navegadores como o Safari.

Bibliotecas de terceiros para lidar com a imutabilidade

Existem vários wa ys para lidar com a imutabilidade em JavaScript. Embora os métodos discutidos acima possam ser úteis, você provavelmente encontrará uma biblioteca para qualquer aplicativo substancial.

Os exemplos incluem Immer e Immutable.js . Com o Immer, você usa os mesmos tipos de dados JavaScript que já conhece. No entanto, embora Immutable.js introduza novas estruturas de dados, pode ser a opção mais rápida.

Conclusão

JavaScript fornece métodos como Object.freeze () e Object.seal () para níveis variáveis ​​de restrição de acesso para objetos.

No entanto, assim como com a clonagem, porque os objetos são copiados por referência, o congelamento geralmente é superficial. Portanto, você pode implementar suas próprias funções básicas de congelamento profundo ou selo profundo ou, dependendo do seu caso de uso, tirar proveito de bibliotecas como Immer ou Immutable.js.