🔏 Imutabilidade de Atribuição VS Imutabilidade de Valor
Dev Maiqui 🇧🇷
Posted on June 28, 2024
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!
Já no código abaixo podemos ver a imutabilidade de atribuição:
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.
- Mutabilidade VS Imutabilidade
- Data Types
- Mutabilidade no JavaScript
- Imutabilidade no JavaScript
- Como evitar a mutabilidade de objetos
- Como evitar a mutabilidade de arrays
- Considerações finais
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.
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.
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
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.
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"]
}
Na stack, você encontrará person
, que é um ponteiro para o objeto real no heap.
const person2 = person
console.log(person)
console.log(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)
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)
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);
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
.
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}
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)
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;
No código acima, uma variável chamada student1
foi criada e atribuída a student2
.
student1 = "Mike"
console.log(student1);
console.log(student2)
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.
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);
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)
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)
Verifique o console - a propriedade designation
não foi adicionada e não há nenhuma mensagem de erro:
- usando o método
defineProperty
Aqui está a sintaxe:
Object.defineProperty(obj, prop, descriptor)
Veja o que está acontecendo no código acima:
-
obj
: O objeto ao qual você deseja adicionar propriedades. -
prop
: Você define o nome da propriedade que deseja adicionar ou alterar. Deve ser uma cadeia de caracteres ou um símbolo -
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)
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
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)
O valor da propriedade de um objeto não extensível pode ser alterado:
Como excluir uma propriedade (Object.preventExtensions
)
Aqui está a sintaxe:
delete object.propertyname
const makeNonExtensive = {
firstname: "Maiqui",
lastname: "Tomé"
}
Object.preventExtensions(makeNonExtensive)
delete makeNonExtensive.lastname
console.log(makeNonExtensive)
Apesar de o objeto não ser extensível, a propriedade lastname
foi excluída:
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)
Como adicionar novas propriedades (Object.seal()
)
const people = {
person1: 'Maiqui',
person2: "Maria",
person3: "Eduardo"
}
Object.seal(people)
console.log(Object.isSealed(people))
Object.isSealed(people)
é usado para verificar se um objeto está selado.
- Como usar a
notação de ponto
people.person4 = "Patrick"
console.log(people)
Sem produzir um erro, a notação de ponto
falha ao adicionar a nova propriedade 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)
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
.
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)
Agora, o person2
foi alterado de "Maria"
para "Maria Clara"
:
Tentando remover uma propriedade existente (Object.seal()
)
const people = {
person1: 'Maiqui',
person2: "Maria",
person3: "Eduardo"
}
Object.seal(people)
delete people.person1
console.log(people)
As propriedades não podem ser removidas dos objetos selados. No console, person1
ainda permanece:
Como usar o método Object.freeze()
Aqui está a sintaxe:
Object.freeze(obj)
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);
ATENÇÃO 🚨
Mesmo quando você aplica oObject.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)
Observe que o player4
não foi adicionado:
- 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)
A notação de ponto falha silenciosamente ao tentar adicionar uma propriedade, mas 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)
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)
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)
A tentativa de excluir uma propriedade em um objeto congelado também falha silenciosamente:
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)
O player6
foi adicionado aos substitutos aninhados
, embora o método Object.freeze()
tenha sido aplicado aos jogadores da equipe principal.
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)
O player5
foi removido. Tudo o que o object.freeze
impede no objeto pai pode ser feito no objeto filho que está aninhado:
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));
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);
Agora, quando você tentar adicionar uma propriedade, receberá este erro Uncaught TypeError: Cannot define property player6, object is not extensible
:
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
:
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.
Era isso... obrigado por ler e até a próxima 🙂👋
Para a construção deste artigo foi utilizado os seguintes conteúdos como base:
Posted on June 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.