Filipe Colaquecez

Filipe Colaquecez

Imutabilidade - Já ouviu falar sobre?

javascript, array, boas práticas, programação funcional

É super importante entender o que é imutabilidade e os benefícios que isso pode trazer para o seu código :)

Imutabilidade é um dos principais pontos do paradigma de programação funcional. Nesse artigo irei falar um pouco sobre o assunto e apresentar alguns cenários do seu uso e o por quê isso é útil. :)

Imutabilidade?

Imutabilidade na programação significa que quando algum dado é criado/instanciado você não consegue alterar o seu valor a partir da sua referência.

Conseguimos encontrar alguns tipos imutáveis no JavaScript, um bom exemplo é o tipo String

let country = "Brazil"

let country2 = country

console.log(country) //Brazil
console.log(country2) //Brazil

country2 = "Japan"

console.log(country) //Brazil
console.log(country2) //Japan

Ué, mas você falou que `strings` são imutáveis mas no exemplo que você mostrou a váriavel mudou de valor. >:(

Parece um pouco confuso né?!

Quando criamos uma String não conseguimos mudar a sua referência porém conseguimos reatribuir novos valores e isso são coisas totalmente diferentes, um exemplo é que se você tentar utilizar algum método na sua String como por exemplo:

let myString = "Cinemark"
myString.toUpperCase() //CINEMARK
console.log(myString) //Cinemark

Perceba que o valor da sua váriavel não mudou, o método apenas te retorna uma nova String e para utilizar você teria que reatribuir o valor ou salvar em uma nova váriavel.

Para ficar um pouco mais claro, quando fazemos isso:

const a = 3
const b = a

Isso cria uma cópia em dois espaços de memória diferentes, e isso acontece porque o number também é imutável.

Vamos continuar, irei mostrar alguns outros exemplos com tipos mutáveis e irá ficar muito mais claro o conteúdo!

Tipos Primitivos

Antes de continuar precisamos entender quais os tipos primitivos do JavaScript, que são: String, Number, BigInt, Boolean, Undefined, Symbol e Null, todos estes são imutáveis.

Reforçando, uma variável pode ser reatribuída com um novo valor, mas a sua referência não pode ser alterada e compartilhada da mesma forma que fazemos com objetos, arrays ou funções.

Recomendação de leitura: Tipos primitivos

Constantes - Objetos e Arrays

Quando usamos const ela impede reatribuir um valor, porém só funciona bem com tipos primitivos, veja os exemplos abaixo:

Lembre-se: Constante(const) não significa que esse dado irá ser imutável, você apenas impede a reatribuição.

const name = "Filipe"
name = "Filipe Pereira"
//Uncaught TypeError: Assignment to constant variable.

const age = 26
age = 27
//Uncaught TypeError: Assignment to constant variable.

Utilizando a mesma técnica para objetos:

const car = { brand: "Fiat" }
car = {}
//Uncaught TypeError: Assignment to constant variable.

Perceba que não conseguimos reatribuir um valor, porém conseguimos alterar diretamente suas propriedades, exemplo:

const car = { brand: "Fiat" }
car.brand = "BMW"
console.log(car) // {brand: 'BMW'}

Side-Effects

Side-Effects são mudanças ou ações que acontecem fora do nosso escopo, ou seja, algum comportamento que não estavamos esperando, para ficar mais claro irei exemplificar um caso:

const person = { name: "Filipe" }

const person2 = person

person2.name = "James"

console.log(person2.name) //James

console.log(person.name) //James

Perceba que quando fizemos a mudança do person2.name isso afetou também nosso objeto person.name, e isso acontece porque eles compartilham a mesma referência, portanto, qualquer propriedade que você mudar automaticamente irá mudar de todos os outros objetos que possuem a mesma referência.

E nesse momento fica mais claro o por quê os tipos primitivos são imutáveis, imagina se fosse possível mudar a referência da letra a por exemplo:

String.a = "b"

Se isso fosse possivel iriamos gerar um efeito colateral em todas as letras a tendo um comportamento não desejavel.

Uma imagem para ficar ainda mais claro o entendimento:

Referência x Valor - Imutabilidade

Imutabilidade se resume bem nesse gif, e por isso quando estamos trabalhando com tipos mutáveis precisamos criar uma nova referência para evitarmos futuros problemas com o nosso código.

Entendi o problema, mas como resolvo?

Podemos utilizar o spread para criarmos um novo objeto, dessa forma iremos criar uma nova referência, logo quando você alterar o valor não irá refletir em outros objetos.

const person = { name: "Filipe" }

const person2 = { ...person }

person2.name = "James"

console.log(person.name) //Filipe

console.log(person2.name) //James

Um outro exemplo comum de acontecer:

const person = {
  name: "Filipe",
  role: {
    name: "dev 1",
  },
}

const person2 = { ...person }

person2.name = "Cleber"

console.log(person.name) //Filipe

console.log(person2.name) //Cleber

Tudo certo até aqui, porém e se agora o person2 tem um novo cargo para dev 2? Provavelmente pensaria em algo do tipo: person2.role.name = 'dev 2'

Vamos testar:

const person = {
  name: "Filipe",
  role: {
    name: "dev 1",
  },
}

const person2 = { ...person }

person2.name = "Cleber"

person2.role.name = "dev 2"

console.log(person.name) //Filipe
console.log(person.role.name) //dev 2

console.log(person2.name) //Cleber
console.log(person2.role.name) //dev 2

Perceba que apenas a raiz do objeto não foi alterada quando fizemos person2.name porém quando fizemos person2.role.name ele alterou de todos os objetos e isso porque o spread ele usa uma técnica chamada shallow cloning que basicamente ele clona apenas a raiz do seu objeto para poupar processamento, e todos os sub-objetos ele usa a referência do objeto principal, então por isso que quando alteramos a role acabamos alterando de todos os objetos.

Para resolver isso podemos usar uma técnica chamada deep cloning que basicamente vamos clonar tanto o objeto raiz como os subs:

const person = {
  name: "Filipe",
  role: {
    name: "dev 1",
  },
}

const person2 = {
  ...person,
  role: {
    ...person.role,
  },
}

person2.name = "New name"

person2.role.name = "dev 2"

console.log(person2.name) //New Name
console.log(person2.role.name) //dev 2

console.log(person.name) //Filipe
console.log(person.role.name) //dev 1

Há várias libs que fazem isso para você de forma automática, o React recomenda a lib immutable.js porque chega um ponto que fica extremamente difícil ficar fazendo esse tipo de tratativa e se você utiliza redux vale a pena dar uma olhada no redux toolkit que já usa internamente o immer.

Conclusão

No decorrer que uma aplicação vai escalando, temos muitas mudanças de estados vindo ou não de chamadas assíncronas, e como vimos, mutabilidade "esconde" mudanças por causa dos side-effects o que torna instável o comportamento da nossa aplicação e tornando muito dificil debugar.

Resumindo, mantendo sua estrutura imutável você consegue prever o que tem no seu estado e ter a certeza que nada mudou no decorrer que a aplicação vai rodando e isso contribui também em uma melhor manutenção do seu código.

Me acompanhe nas redes

Instagram:

@colaquecez.dev

Compartilhe ;)