E aí, galera 👋
Eu estava assistindo um vídeo de Programação Orientada a Objetos em JavaScript e achei que seria bom compartilhar o que estou aprendendo / revisando. É por isso que estou planejando uma série de postagens para cobrir alguns conceitos como uma forma de aprender mais sobre os tópicos e espero que também ajude algumas pessoas.
Neste artigo, abordarei os tipos de Javascript e suas diferenças.
Tipos do Javascript
Existem oito tipos de dados em Javascript:
- string
- number
- bigint
- boolean
- undefined
- null
- symbol
- Object
Os primeiros 7 deles são comumente chamados de Tipos primitivos e todo o resto são Tipo objeto.
Tipos primitivos
Eles só podem armazenar um único dado, não têm métodos e são imutáveis.
Espera ae, como assim? Eles são mutáveis ... Na verdade, eles não são. Normalmente confundimos o próprio valor primitivo com a variável que atribuímos ao valor primitivo. Dá uma olhada:
// A gente nao pode modificar a string
let car = "car"
console.log(car) // car
car.toUpperCase()
console.log(car) // car
car[0] = "b"
console.log(car) // car
// Mas podemos atribuir um novo valor à mesma variável
car = car.toUpperCase()
console.log(car) // CAR
A variável pode ser reatribuída a um novo valor, mas o valor primitivo existente não pode ser alterado como fazemos com arrays ou objetos.
Portanto, esta é uma das principais diferenças entre os dois tipos:
Tipos primitivos são imutáveis e tipos de objetos são mutáveis.
_Aah, beleza. Entendi! Mas como é que eles não têm métodos, se você acabou de usar um? _
Esse é outro ponto interessante! Tipos primitivos
não têm métodos, mas, exceto pelo null
e undefined
, todos eles têm objetos equivalentes que envolvem os valores primitivos, assim podemos usar métodos.
Para o tipo primitivo string
existe o objeto String
, para a primitiva number
existe Number
, e portanto existem Boolean
, BigInt
e Symbol
.
Javascript converte automaticamente os primitivos em seus objetos correspondentes quando um método for invocado. Javascript envolve o primitivo e chama o método.
Veja abaixo como um objeto String
é estruturado com seu valor primitivo e __proto__
(que está além do nosso escopo aqui, mas está relacionado ao prototype do seu objeto) com os métodos associados:
É assim que podemos acessar propriedades como length
e métodos como indexOf
e substring
ao trabalhar com tipos primitivos string
.
Quando o Javascript os envolve com seus objetos correspondentes, ele chama o método valueOf
para converter o objeto de volta ao valor primitivo quando o Javascript encontra um objeto onde um valor primitivo é esperado.
Tipos de objeto
Diferentemente dos tipos primitivos, os objetos podem armazenar coleções de dados, as suas propriedades, e são mutáveis.
// Podemos modificar os objetos sem precisar reatribui-los à variáveis
let cars = ["bmw", "toyota"]
console.log(cars) // ["bmw", "toyota"]
cars.push("tesla")
console.log(cars) // ["bmw", "toyota", "tesla"]
let car = { brand: "tesla" }
car.year = 2021
console.log(car) // { brand: "tesla", year: "2021" };
Exemplos de tipos de Objeto
são Array e o próprio Object. Diferente dos Tipos primitivos
, eles possuem métodos embutidos. Dá para ver abaixo como um array e um objeto são mostrados aqui no navegador com alguns de seus métodos:
Por mais estranho que pareça, funções
são na verdade objetos também, são objetos Function
, que podem ser chamados.
Só para ilustrar isso e por curiosidade, veja como as funções também podem ser criadas:
Isso é apenas para fins educacionais, uma vez que não é recomendado usá-lo dessa forma e há problemas com closures, conforme mostrado aqui.
Beleza, aprendemos um pouco mais sobre esses tipos, então vamos ver algumas das diferenças ao trabalhar com eles.
Diferenças entre os tipos
1. Atribuindo a uma variável e copiando o valor
A diferença na forma como os valores são armazenados nas variáveis é o que faz as pessoas geralmente chamarem Tipos de objeto
como Tipos de referência
.
Tipos primitivos
Quando atribuímos um tipo primitivo a uma variável, podemos pensar nessa variável como contendo aquele valor primitivo.
let car = "tesla"
let year = 2021
// Variável - Valor
// car - "tesla"
// year - 2021
Portanto, quando atribuímos essa variável a outra variável, estamos copiando esse valor para a nova variável. Assim, tipos primitivos são "copiados por valor".
let car = "tesla"
let newCar = car
// Variável - Valor
// car - "tesla"
// newCar - "tesla"
Uma vez que copiamos os valores primitivos diretamente, ambas as variáveis são separadas e se mudarmos uma não afetamos a outra.
let car = "tesla"
let newCar = car
car = "audi"
// Variável - Valor
// car - "audi"
// newCar - "tesla"
Tipos de objeto
Com Tipos de Objeto
as coisas são diferentes. Quando atribuímos um objeto a uma variável, a variável recebe uma referência para esse valor. Esta referência armazena o endereço
para a localização desse valor na memória (tecnicamente mais do que isso, mas vamos simplificar). Portanto, a variável não tem o valor em si.
Vamos imaginar a variável, o valor que ela armazena, o endereço na memória e o objeto nos trechos abaixo:
let cars = ["tesla"]
// Variável - Valor - Endereço - Objeto
// cars - <#001> (A referência) - #001 - ["tesla"]
Desta forma, ao atribuirmos esta variável a outra estamos dando a ela a referência do objeto e não copiando o próprio objeto como acontece com o valor primitivo. Assim, tipos de objetos são "copiados por referência".
let cars = ["tesla"]
let newCars = cars
// Variável - Valor - Endereço - Objeto
// cars - <#001> (A referência) - #001 - ["tesla"]
// newCars - <#001> (A referência tem o mesmo endereço)
cars = ["tesla", "audi"]
// Variable - Valor - Endereço - Objeto
// cars - <#001> (A referência) - #001 - ["tesla", "audi"]
// newCars - <#001> (A referência tem o mesmo endereço)
console.log(cars) // ["tesla", "audi"]
console.log(newCars) // ["tesla", "audi"]
Ambos têm referências ao mesmo objeto array. Então quando modificamos o objeto de uma das variáveis a outra também terá essa mudança.
2. Comparação
Entender as diferenças do que é armazenado nas variáveis ao lidar com tipos primitivos e de objeto é crucial para entender como podemos compará-los.
Tipos primitivos
Usando o operador de comparação estrita ===
, se compararmos duas variáveis que armazenam valores primitivos elas serão iguais se tiverem o mesmo valor.
let year = 2021
let newYear = 2021
console.log(year === 2021) // True
console.log(year === newYear) // True
No entanto, se compararmos duas variáveis que foram definidas como Tipos de objeto
, estaremos na verdade comparando duas referências em vez de seus objetos. Portanto, eles são iguais apenas se referirem exatamente ao mesmo objeto.
let cars = ["tesla"]
let newCars = ["tesla"]
console.log(cars === newCars) // False
console.log(cars === ["tesla"]) // False
// Agora copiamos a referência de cars para newCars
newCars = cars
console.log(cars === newCars) // True
Mesmo que, no início do trecho de código estivéssemos trabalhando com o mesmo conteúdo nos arrays, as variáveis não tinham as mesmas referências, elas tinham referências a diferentes objetos de array na memória. Porém, após copiarmos a referência para newCars
, já que agora eles estão "apontando" para o mesmo objeto, a avaliação é True
.
Portanto, para comparar objetos, não podemos simplesmente usar o operador ===
porque, embora eles possam ter as mesmas propriedades, eles podem não fazer referência ao mesmo objeto. Existem algumas maneiras de fazer isso e, por isso, recomendo a leitura deste artigo (que está em inglês).
3. Passando para funções
Quando passamos tipos primitivos ou de objetos para funções, é como se estivéssemos copiando seus valores/referências para os parâmetros das funções, da mesma maneira que estivéssemos atribuindo a eles com =
.
Como vimos que quando os atribuímos a novas variáveis estamos ou copiando seu valor (para tipos primitivos) ou referenciando (para tipos de objeto), é mais fácil entender o que acontece com funções e seu escopo externo.
Tipos primitivos
Quando estamos passando Tipos primitivos
para funções, estamos copiando seus valores para os parâmetros das funções, assim isso não afeta a variável inicial no escopo externo.
let year = 2021
function getYearWithoutCovid (freeYear) {
freeYear = 2022
return freeYear
}
const newYear = getYearWithoutCovid(year)
console.log(year) // 2021
console.log(newYear) // 2022
Passando ano
para a função, estamos copiando seu valor para o parâmetro da função (freeYear
será 2021), assim a variável original não seja afetada.
Tipos de objeto
Com Tipos de objeto
, estamos copiando suas referências ao passá-los como parâmetros de funções. Portanto, se alterarmos o objeto dentro da função isso também será sentido no escopo externo.
let person = { name: "Paul", status: "unemployeed" }
function getAJob (person) {
person.status = "employeed"
return person
}
const newPerson = getAJob(person)
console.log(person) // { name: "Paul", status: "employeed" }
console.log(newPerson) // { name: "Paul", status: "employeed" }
Quando passamos pessoa
para a função, estamos copiando sua referência para o parâmetro da função, não seu valor de objeto. Alterá-lo dentro da função afetará o objeto inicial no escopo externo, uma vez que ambas as variáveis têm referências ao mesmo objeto.
É por isso que é recomendado usar Funçoes puras
neste caso (que não estão no escopo deste artigo, mas eu encorajo você a pesquisar sobre isso <3). Para isso, criamos uma cópia local dessa pessoa
dentro da função e a modificamos ao invés do objeto passado.
Conclusão
Espero que com este artigo você possa entender um pouco mais sobre os tipos de dados em Javascript e também aprender as principais diferenças entre eles.
Eu apenas tentei compartilhar o que aprendi ao revisar esses conceitos, então há mais coisas a acrescentar, mas achei que essa era uma forma didática de explicar. Se você tem coisas a acrescentar e discutir, deixe um comentário :) Se isso te ajudou de alguma forma, deixe um coração <3
Além disso, me segue no Twitter se quiser, talvez compartilhe coisas legais lá também :)
Referências
https://262.ecma-international.org/11.0/#sec-ecmascript-data-types-and-values
https://flaviocopes.com/difference-primitive-types-objects/
https://dmitripavlutin.com/value-vs-reference-javascript
https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0
https://codeburst.io/javascript-essentials-types-data-structures-3ac039f9877b#01e0
https://mattgreer.dev/blog/javascript-is-a-pass-by-value-language/
https://developer.mozilla.org/en-US/docs/Glossary/Primitive
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf
Top comments (1)
Artigo muito interessante e gostei bastante. Só fiquei pensando se de fato para os tipos primitivos seus valores são copiados, visto que eles são constantes, tendo que ser reatribuídos para mudar de valor, eles também poderiam ser implementados com cópia de referência. Para valores que cabem um poucos bits, isso não faria tanta diferença, apesar de poder consumir até mais memória se precisar de mais bits para guardar o endereço do que o valor, porém para
strings
, isso poderia gerar uma economia de memória se ela tiver muitos caracteres, e sendo mais fácil de verificar se são iguais, podendo comparar seus endereços, em vez de comparar caracter a caracter. No Python, valores inteiros pequenos são guardados em cache e usado sempre o mesmo objeto (visto que no Python tudo é um objeto). Só não sei o quanto isso impacta no garbage collector, que precisaria lidar com mais objetos, ou poderia aumentar o consumo de memória, guardando valores que não estão mais em uso.