DEV Community

Rafael Leitão
Rafael Leitão

Posted on

De volta ao básico: Tipos Primitivos e Objetos em Javascript

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:

  1. string
  2. number
  3. bigint
  4. boolean
  5. undefined
  6. null
  7. symbol
  8. 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
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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

Alt Text

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" };
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
eduardoklosowski profile image
Eduardo Klosowski

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.