DEV Community

Cover image for 🔏Imutabilidade de Atribuição VS Imutabilidade de Valor
Dev Maiqui 🇧🇷
Dev Maiqui 🇧🇷

Posted on

🔏Imutabilidade de Atribuição VS Imutabilidade de Valor

Sim! Podemos alterar o valor de uma const quando o tipo de dado atribuído a ela é de um array ou objeto. No código abaixo podemos ver que o valor do array foi modificado. Isso não é imutabilidade de valor!

alterando o valor de um array atribuído a uma const

Já no código abaixo podemos ver a imutabilidade de atribuição:

tentando atribuir outro valor a uma const

Vamos entender melhor como isso funciona por debaixo dos panos; sobre imutabilidade no JavaScript e como evitar que um array ou objeto atribuído em uma const seja alterado.

A Jornada do Autodidata em Inglês

Mutabilidade VS Imutabilidade

No JavaScript, diferentes tipos de dados (data types em inglês) têm diferentes comportamentos e locais na memória. Portanto, para reduzir as chances de ter bugs em seu código, você precisa entender o conceito de mutabilidade e imutabilidade em JavaScript.

Mutabilidade refere-se a tipos de dados que podem ser acessados e alterados após serem criados e armazenados na memória. A imutabilidade, por outro lado, refere-se a tipos de dados que não podem ser alterados após a criação, mas que ainda podem ser acessados na memória.


Data Types

Os tipos de dados (data types em inglês) são categorizados em tipos primitivos e de referência no JavaScript. Antes de explicar essas categorias, vamos dar uma olhada em dois termos importantes relacionados à memória que você precisará conhecer: a Stack e a Heap.

O que é a Stack?

A Stack (pilha em português) é uma estrutura de dados que obedece ao princípio Last In First Out (LIFO). Isso significa que o último item a entrar na pilha sai primeiro.

Last In First Out (LIFO)
Imagem acima da Wikipédia.

O que é Heap?

O Heap é usado apenas em variáveis do tipo de dados de referência.

O Heap guarda o valor real atribuído à variável. A variável guarda o endereço de memória do Heap. Quando o valor é atribuído à variável, a variável (armazenando o endereço de memória do Heap) é colocada na pilha por cima, por último.

Exemplo Heap e Stack

Tipos de dados primitivos

Os tipos de dados primitivos são imutáveis e não são objetos porque não têm propriedades e métodos: string, number, BigInt, boolean, undefined, Symbol, null.

🤔 Espere um minuto, você pode pensar – eu mudo os valores das variáveis ​​primitivas o tempo todo!

Bem, pode parecer que você está modificando um valor, mas esse não é realmente o caso. Vamos mostrar um exemplo:

let greet = "Hello";
greet += ", World";  
console.log(greet); // Hello, World
Enter fullscreen mode Exit fullscreen mode

A primeira linha deste código cria a string Hello e a atribui à variável greet. A segunda linha anexa , World a essa string. Parece que estamos mudando a string greet, mas o JavaScript não altera a string, em vez disso, ele cria uma nova string.

atribuindo valor a variavel greet

Tipos de dados de referência

Por padrão, os tipos de dados de referência são mutáveis. Os tipos de dados de referência consistem em funções, arrays e objetos.

Os tipos de dados de referência colocam a variável na stack. A variável funciona como um ponteiro que aponta para o objeto localizado na heap.

A principal diferença entre essas categorias é que os tipos primitivos são imutáveis, mas os tipos de referência são mutáveis.


Mutabilidade no JavaScript

Se um tipo de dado for mutável, significa que você pode alterá-lo. A mutabilidade permite que você modifique os valores existentes sem criar novos valores.

Para cada objeto, um ponteiro é adicionado à stack, e esse ponteiro aponta para o objeto no heap.

Veja, por exemplo, o código a seguir:

const person = {
         name: "Mike Shinoda",
         age: 47,
         Hobbies: ["cantar", "tocar guitarra"]
   }
Enter fullscreen mode Exit fullscreen mode

Na stack, você encontrará person, que é um ponteiro para o objeto real no heap.

const person2 = person

console.log(person)

console.log(person2)
Enter fullscreen mode Exit fullscreen mode

person2

Outro ponteiro é colocado na stack quando person é atribuído a person2. Agora, esses ponteiros apontam para um único objeto no heap.

Os dados de referência não copiam valores, mas sim ponteiros.

person2.age = 53;

console.log(person)

console.log(person2)
Enter fullscreen mode Exit fullscreen mode

Alterando a idade

Alterar a idade de person2 atualiza a idade do objeto person. Agora você sabe que isso ocorre porque ambos apontam para o mesmo objeto.


Como clonar propriedades de objetos

O método object.assign copia propriedades de um objeto (a origem) para outro objeto (o destino) e retorna o objeto de destino modificado.

Veja a seguir a sintaxe:

Object.assign(target, source)
Enter fullscreen mode Exit fullscreen mode

O método tem dois argumentos, target e source. O target é o objeto que recebe as novas propriedades, enquanto a source é de onde vêm as propriedades. O target pode ser um objeto vazio {}.

Em uma situação em que o source e o target compartilham a mesma chave (key em inglês), o objeto de origem substitui o valor da chave no target.

const person = {
         name: "Mike Shinoda",
         age: 47,
         Hobbies: ["cantar", "tocar guitarra"]
   }

const person2 = Object.assign({}, person);
Enter fullscreen mode Exit fullscreen mode

As propriedades do objeto person foram clonadas em um target vazio.

person2 agora tem suas próprias propriedades. Você pode comprovar isso alterando o valor de qualquer uma de suas propriedades. Essa alteração não afetará os valores das propriedades do objeto de person.

Alterando person2

O valor de person2.age que foi alterado para 53 não afeta de forma alguma o valor de person.age porque ambos têm suas próprias propriedades.

Usando o Spread Operator

Esta é a sintaxe do Spread Operator:

const newObj = {...obj}
Enter fullscreen mode Exit fullscreen mode

Usar o operador spread é bastante simples. Você precisa colocar três pontos ... antes do nome do objeto cujas propriedades você pretende clonar:

const person = {
         name: "Mike Shinoda",
         age: 47,
         Hobbies: ["cantar", "tocar guitarra"]
   }

const person2 = {...person};

person2.age = 53;

console.log(person)

console.log(person2)
Enter fullscreen mode Exit fullscreen mode

Usando o Spread Operator


Imutabilidade no JavaScript

Imutabilidade é o estado em que os valores são imutáveis (ou seja, não podem ser alterados). Um valor é imutável quando é impossível alterá-lo. Os tipos de dados primitivos são imutáveis, como discutimos acima.

Vamos dar uma olhada em um exemplo:

let student1 = "Maiqui";

let student2 = student1;
Enter fullscreen mode Exit fullscreen mode

No código acima, uma variável chamada student1 foi criada e atribuída a student2.

 student1 = "Mike"

 console.log(student1);

 console.log(student2)
Enter fullscreen mode Exit fullscreen mode

A alteração de student1 para Mike não altera o valor inicial de student2. Isso prova que, nos tipos de dados primitivos, os valores reais são copiados, portanto, ambos têm seus próprios valores. Na memória stack, student1 e student2 são diferentes.

Alterando student1

A stack obedece ao princípio Last-In-First-Out (último a entrar, primeiro a sair). O primeiro item que entra na stack é o último a sair e vice-versa. Assim, acessar aos itens armazenados na stack fica fácil.


Como evitar a mutabilidade de objetos

Até agora, você aprendeu que os objetos são mutáveis por padrão.

const people = {
           person1: 'Mike',
           person2: 'Chester',
           person3: 'Joe'
   }


   Object.defineProperty(people, "person4", {
      value: "Brad",
   })

   console.log(people);
Enter fullscreen mode Exit fullscreen mode

Agora adicionamos o person4.

Para evitar a mutabilidade do objeto, você pode usar os métodos Object.preventExtensions(), Object.seal() e Object.freeze().

Para todos os três métodos, exploraremos a adição de propriedades usando a notação de ponto e a propriedade define, a modificação de propriedades usando defineProperty e a exclusão de propriedades.

Isso lhe dará uma melhor compreensão dos recursos e das limitações de cada método e, por fim, o ajudará a determinar qual método é mais adequado para um caso de uso específico.

Portanto, vamos nos aprofundar e explorar esses métodos com mais detalhes.

Como usar o método Object.preventExtensions

Veja a seguir a sintaxe desse método:

Object.preventExtensions(obj)
Enter fullscreen mode Exit fullscreen mode

O uso do Object.preventExtensions impede que novas propriedades entrem no objeto. O objeto não aumenta de tamanho e mantém suas propriedades. Por padrão, todos os objetos em JavaScript são extensíveis. Com esse método, você pode excluir propriedades do seu objeto.

Tentando adicionar novas propriedades (Object.preventExtensions)

  • usando a notação de ponto (dot notation):
const makeNonExtensive = {
           firstname: "Maiqui",
           lastname: "Tomé"
   }

   Object.preventExtensions(makeNonExtensive)

   makeNonExtensive.designation = "Software Engineer";

   console.log(makeNonExtensive)
Enter fullscreen mode Exit fullscreen mode

Verifique o console - a propriedade designation não foi adicionada e não há nenhuma mensagem de erro:

usando  raw `dot notation` endraw  em preventExtensions

  • usando o método defineProperty

Aqui está a sintaxe:

Object.defineProperty(obj, prop, descriptor)
Enter fullscreen mode Exit fullscreen mode

Veja o que está acontecendo no código acima:

  1. obj: O objeto ao qual você deseja adicionar propriedades.
  2. prop: Você define o nome da propriedade que deseja adicionar ou alterar. Deve ser uma cadeia de caracteres ou um símbolo
  3. descriptor: você inclui o valor da propriedade.
const makeNonExtensive = {
           firstname: "Maiqui",
           lastname: "Tomé"
   }

   Object.preventExtensions(makeNonExtensive)

   Object.defineProperty(makeNonExtensive, "age", {
      value: 18,
   })

   console.log(makeNonExtensive)
Enter fullscreen mode Exit fullscreen mode

A adição de novas propriedades usando a propriedade define gera esta mensagem de erro: Uncaught TypeError: Cannot define property age, object is not extensible

Usando defineProperty para adicionar

Como modificar uma propriedade existente usando defineProperty (Object.preventExtensions)

const makeNonExtensive = {
            firstname: "Maiqui",
            lastname: "Tomé"
    }

Object.preventExtensions(makeNonExtensive)

Object.defineProperty(makeNonExtensive, 'firstname', {
  value: 'Mike',
})

console.log(makeNonExtensive)
Enter fullscreen mode Exit fullscreen mode

O valor da propriedade de um objeto não extensível pode ser alterado:

Usando defineProperty para modificar

Como excluir uma propriedade (Object.preventExtensions)

Aqui está a sintaxe:

delete object.propertyname
Enter fullscreen mode Exit fullscreen mode
const makeNonExtensive = {
   firstname: "Maiqui",
   lastname: "Tomé"
}

Object.preventExtensions(makeNonExtensive)

delete makeNonExtensive.lastname

console.log(makeNonExtensive)
Enter fullscreen mode Exit fullscreen mode

Apesar de o objeto não ser extensível, a propriedade lastname foi excluída:

excluindo uma propriedade

Como usar o método Object.seal()

Todos os objetos em Javascript são extensíveis por padrão. Como o nome sugere, esse método sela um objeto. Não é possível adicionar novas propriedades a um objeto selado ou excluir uma propriedade existente de um objeto selado. Mas o object.seal permite modificar as propriedades existentes.

Aqui está a sintaxe:

Object.seal(obj)
Enter fullscreen mode Exit fullscreen mode

Como adicionar novas propriedades (Object.seal())

const people = {
  person1: 'Maiqui',
  person2: "Maria", 
  person3: "Eduardo"
}

Object.seal(people)

console.log(Object.isSealed(people))
Enter fullscreen mode Exit fullscreen mode

Object.isSealed(people) é usado para verificar se um objeto está selado.

  • Como usar a notação de ponto
people.person4 = "Patrick"

console.log(people)
Enter fullscreen mode Exit fullscreen mode

Sem produzir um erro, a notação de ponto falha ao adicionar a nova propriedade person4:

people.person4 =

  • Usando o método defineProperty (Object.seal())
const people = {
  person1: 'Maiqui',
  person2: "Maria", 
  person3: "Eduardo"
}

Object.seal(people)

Object.defineProperty(people, 'person4', {
  value: 'Patrick'
})

console.log(people)
Enter fullscreen mode Exit fullscreen mode

A mensagem de erro "Uncaught TypeError: Cannot define property student4, the object is not extendable" é lançada quando se tenta adicionar a mesma propriedade usando o método defineProperty.

Usando o método  raw `defineProperty` endraw

Como modificar uma propriedade existente usando defineProperty (Object.seal())

const people = {
  person1: 'Maiqui',
  person2: "Maria", 
  person3: "Eduardo"
}

Object.seal(people)

Object.defineProperty(people, 'person2', {
  value: 'Maria Clara',
})

console.log(people)
Enter fullscreen mode Exit fullscreen mode

Agora, o person2 foi alterado de "Maria" para "Maria Clara":

alterando de  raw `

Tentando remover uma propriedade existente (Object.seal())

const people = {
  person1: 'Maiqui',
  person2: "Maria", 
  person3: "Eduardo"
}

Object.seal(people)

delete people.person1

console.log(people)
Enter fullscreen mode Exit fullscreen mode

As propriedades não podem ser removidas dos objetos selados. No console, person1 ainda permanece:

tentando remover propriedade

Como usar o método Object.freeze()

Aqui está a sintaxe:

Object.freeze(obj)
Enter fullscreen mode Exit fullscreen mode

O método Object.freeze() congela um objeto. O uso desse método garante alta integridade ao assegurar que não será possível extrair, modificar propriedades existentes ou adicionar novas propriedades ao objeto.

Para verificar se um objeto está congelado, use a sintaxe abaixo:

Object.isFrozen(obj);
Enter fullscreen mode Exit fullscreen mode

ATENÇÃO 🚨
Mesmo quando você aplica o Object.freeze() a um objeto, é possível adicionar uma nova propriedade, modificar uma propriedade existente ou excluir propriedades de objetos aninhados sob ele.

Assim como fizemos com outros métodos, vamos explorar o método Object.freeze() em relação à adição de novas propriedades, modificação de valores ou exclusão de propriedades de um objeto.

Tentando adicionar novas propriedades (Object.freeze())

  • Usando dot notation
const sportClubInternacional = {
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré"
}

Object.freeze(sportClubInternacional)

sportClubInternacional.player4 = "Alan Patrick"

console.log(sportClubInternacional)
Enter fullscreen mode Exit fullscreen mode

Observe que o player4 não foi adicionado:

sportClubInternacional.player4

  • Usando o método defineProperty
const sportClubInternacional = {
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré"
}

Object.freeze(sportClubInternacional)

Object.defineProperty(sportClubInternacional, 'player4', {
  value: "Alan Patrick"
})

console.log(sportClubInternacional)
Enter fullscreen mode Exit fullscreen mode

A notação de ponto falha silenciosamente ao tentar adicionar uma propriedade, mas defineproperty gera um TypeError:

defineproperty gera um TypeError

Tentando modificar propriedades (Object.freeze())

const sportClubInternacional = {
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré"
}

Object.freeze(sportClubInternacional)

sportClubInternacional.player1 = "Enner Valencia - Atacante"

console.log(sportClubInternacional)
Enter fullscreen mode Exit fullscreen mode

sportClubInternacional.player1 =

O código acima falhou silenciosamente, mas com a propriedade defineProperty abaixo, um typeError é lançado.

const sportClubInternacional = {
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré"
}

Object.freeze(sportClubInternacional)

Object.defineProperty(sportClubInternacional, 'player1', {
  value: "Enner Valencia - Atacante"
})

console.log(sportClubInternacional)
Enter fullscreen mode Exit fullscreen mode

Object.defineProperty

Tentando deletar propriedades (Object.freeze())

const sportClubInternacional = {
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré"
}

Object.freeze(sportClubInternacional)

delete sportClubInternacional.player1

console.log(sportClubInternacional)
Enter fullscreen mode Exit fullscreen mode

A tentativa de excluir uma propriedade em um objeto congelado também falha silenciosamente:

delete sportClubInternacional.player1


Como usar Deep Freeze

const sportClubInternacional = {
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré",
      substitutes: {
         player4: "Lucas Alario",
         player5: "Thiago Maia"
      }
}

Object.freeze(sportClubInternacional)

Object.defineProperty(sportClubInternacional.substitutes, 'player6', {
  value: "Alan Patrick"
})

console.log(sportClubInternacional)
Enter fullscreen mode Exit fullscreen mode

O player6 foi adicionado aos substitutos aninhados, embora o método Object.freeze() tenha sido aplicado aos jogadores da equipe principal.

O player6 foi adicionado

Deletando o jogador reserva aninhado

const sportClubInternacional = {
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré",
      substitutes: {
         player4: "Lucas Alario",
         player5: "Thiago Maia"
      }
}

Object.freeze(sportClubInternacional)

delete sportClubInternacional.substitutes.player5

console.log(sportClubInternacional)
Enter fullscreen mode Exit fullscreen mode

O player5 foi removido. Tudo o que o object.freeze impede no objeto pai pode ser feito no objeto filho que está aninhado:

Deletando o jogador reserva aninhado player5

Para evitar isso, empregamos a técnica de congelamento profundo (Deep Freeze em inglês), conforme mostrado abaixo:

Aplicando o Deep Freeze

const deepVal = obj => {
   Object.keys(obj).forEach(prop => {
      if (typeof obj[prop] === 'object') deepVal(obj[prop]);
   });

   return Object.freeze(obj);
};

const sportClubInternacional = deepVal({
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré",
      substitutes: {
         player4: "Lucas Alario",
         player5: "Thiago Maia"
      }
})

Object.freeze(sportClubInternacional)

console.log(Object.isFrozen(sportClubInternacional));
Enter fullscreen mode Exit fullscreen mode

adicionando deepVal

Tentando adicionar uma nova propriedade ao objeto filho

const deepVal = obj => {
   Object.keys(obj).forEach(prop => {
      if (typeof obj[prop] === 'object') deepVal(obj[prop]);
   });

   return Object.freeze(obj);
};

const sportClubInternacional = deepVal({
  player1: "Enner Valencia",
  player2: "Sergio Rochet", 
  player3: "Rafael Borré",
      substitutes: {
         player4: "Lucas Alario",
         player5: "Thiago Maia"
      }
})

Object.freeze(sportClubInternacional)

Object.defineProperty(sportClubInternacional.substitutes, 'player6', {
   value: "Alan Patrick"
})

console.log(sportClubInternacional);
Enter fullscreen mode Exit fullscreen mode

Agora, quando você tentar adicionar uma propriedade, receberá este erro Uncaught TypeError: Cannot define property player6, object is not extensible:

deep freeze funcionando

Além disso, o Deep Freeze também impede que você altere e exclua propriedades de um objeto.


Como evitar a mutabilidade de arrays

A mutabilidade em arrays segue o mesmo princípio dos objetos. Vou mostrar aqui só um exemplo pois é bem parecido com os exemplos de objetos já que ambos são tipos de dados de referência:

Exemplos com array


Considerações finais

Neste artigo você viu sobre os vários tipos de dados e se eles são imutáveis ou mutáveis por padrão.

Você viu que quando você usa const você deve se preocupar com os tipos de dados de referencia que são mutáveis.

Os objetos podem ser alterados por padrão. Mas o uso de métodos específicos, como Object.seal, Object.freeze e preventExtensions, pode impedir a mutabilidade.

O nível de imutabilidade fornecido por esses métodos varia, portanto, certifique-se de usar aquele que corresponde ao nível de integridade que você deseja atingir.

Até a próxima!


Para a construção deste artigo foi utilizado os seguintes conteúdos como base:

Formação TS

Top comments (0)