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:
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.